diff --git a/src/com/xilinx/rapidwright/examples/PrintVersalTimingPaths.java b/src/com/xilinx/rapidwright/examples/PrintVersalTimingPaths.java new file mode 100644 index 000000000..744cd036c --- /dev/null +++ b/src/com/xilinx/rapidwright/examples/PrintVersalTimingPaths.java @@ -0,0 +1,111 @@ +/* + * Copyright (c) 2026, Advanced Micro Devices, Inc. + * All rights reserved. + * + * This file is part of RapidWright. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.xilinx.rapidwright.examples; + +import java.util.List; + +import org.jgrapht.GraphPath; +import org.jgrapht.alg.shortestpath.KShortestSimplePaths; + +import com.xilinx.rapidwright.design.Design; +import com.xilinx.rapidwright.design.Net; +import com.xilinx.rapidwright.device.Node; +import com.xilinx.rapidwright.device.PIP; +import com.xilinx.rapidwright.timing.TimingEdge; +import com.xilinx.rapidwright.timing.TimingGraph; +import com.xilinx.rapidwright.timing.TimingManager; +import com.xilinx.rapidwright.timing.TimingVertex; + +/** + * Loads a placed/routed DCP, builds a TimingGraph (topology-only mode on Versal), + * enumerates K simple paths between the super-source and super-sink, and prints + * each path's hierarchical pin sequence along with the carrying net's PIPs. + * + * Usage: PrintVersalTimingPaths [dcp] [numPaths] + * default dcp: test/RapidWrightDCP/picoblaze_2022.2.dcp + * default numPaths: 100 + */ +public class PrintVersalTimingPaths { + + public static void main(String[] args) { + String dcp = args.length > 0 + ? args[0] + : "test/RapidWrightDCP/picoblaze_2022.2.dcp"; + int k = args.length > 1 ? Integer.parseInt(args[1]) : 100; + + Design design = Design.readCheckpoint(dcp); + + // On Versal this drops into topology-only mode automatically (delays = 0, + // structure intact). TimingManager.postBuild() has already wired + // superSource -> FF outputs and FF inputs -> superSink. + TimingManager tm = new TimingManager(design); + TimingGraph tg = tm.getTimingGraph(); + + // Enumerate K simple paths between the super-source and super-sink. + // (TimingGraph.buildGraphPaths(N) currently only returns one Bellman-Ford + // path regardless of N; calling KShortestSimplePaths directly is the + // clean way to get many paths.) + KShortestSimplePaths kSP = new KShortestSimplePaths<>(tg); + List> paths = + kSP.getPaths(tg.superSource, tg.superSink, k); + + System.out.println("Enumerated " + paths.size() + " path(s).\n"); + + int idx = 0; + for (GraphPath path : paths) { + List edges = path.getEdgeList(); + System.out.println("=== Path #" + (idx++) + " (" + edges.size() + " edges) ==="); + + for (TimingEdge e : edges) { + TimingVertex src = e.getSrc(); + TimingVertex dst = e.getDst(); + + // Skip the super-source / super-sink connector edges. + if ("superSource".equals(src.getName())) continue; + if ("superSink".equals(dst.getName())) continue; + + System.out.println(" " + src.getName() + " -> " + dst.getName()); + + if (e.getFirstPin() != null) { + System.out.println(" src SitePin: " + e.getFirstPin() + + " tile=" + e.getFirstPin().getTile()); + } + if (e.getSecondPin() != null) { + System.out.println(" dst SitePin: " + e.getSecondPin() + + " tile=" + e.getSecondPin().getTile()); + } + + Net net = e.getNet(); + if (net != null && net.hasPIPs()) { + System.out.println(" net: " + net.getName() + + " (" + net.getPIPs().size() + " PIPs)"); + for (PIP pip : net.getPIPs()) { + Node s = pip.getStartNode(); + Node t = pip.getEndNode(); + System.out.println(" " + pip.getTile() + + ": " + s + " [" + s.getIntentCode() + "]" + + " -> " + t + " [" + t.getIntentCode() + "]"); + } + } + } + System.out.println(); + } + } +} diff --git a/src/com/xilinx/rapidwright/timing/DelayModelBuilder.java b/src/com/xilinx/rapidwright/timing/DelayModelBuilder.java index 971c3d273..ea7715d7d 100644 --- a/src/com/xilinx/rapidwright/timing/DelayModelBuilder.java +++ b/src/com/xilinx/rapidwright/timing/DelayModelBuilder.java @@ -30,6 +30,10 @@ import java.io.IOException; import java.util.ArrayList; import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import com.xilinx.rapidwright.util.FileTools; /** * Build a delay model. @@ -53,42 +57,36 @@ class DelayModelBuilder { */ private static String[] valid_source = {"text"}; - private static DelayModel aModel = null; + private static final Map models = new ConcurrentHashMap<>(); /** - * Prepare the appropriate input file for {@link #getDelayModel(String, String, String)} + * Returns the cached DelayModel for a series, constructing it on first access. + * When no intrasite_delay_terms.txt ships for the series (e.g., Versal), a + * topology-only NullDelayModel is returned so TimingGraph can still build a + * structural graph. */ public static DelayModel getDelayModel(String series) { - String fileName = TimingModel.TIMING_DATA_DIR + File.separator +series+ - File.separator + "intrasite_delay_terms.txt"; - return getDelayModel("small", "text", fileName); - } - - /** - * The method that decides to build a new model or to return the existing one. - * Please see the method newDelayModel for parameters' description. - */ - private static DelayModel getDelayModel(String mode, String source, String fileName) { - if (aModel == null) { - synchronized (DelayModelBuilder.class) { - if (aModel == null) { - newDelayModel(mode, source, fileName); - } + return models.computeIfAbsent(series, s -> { + String fileName = TimingModel.TIMING_DATA_DIR + File.separator + s + + File.separator + "intrasite_delay_terms.txt"; + File f = new File(FileTools.getRapidWrightPath() + File.separator + fileName); + if (!f.exists()) { + return NullDelayModel.INSTANCE; } - } - return aModel; + return newDelayModel("small", "text", fileName); + }); } /** - * The method to build DelayModel and DelayModelSource according to the given parameters. + * Build a DelayModel and DelayModelSource according to the given parameters. * @param mode The type of delay model. It defines how data are stored which will affect * the memory requirement and how fast the lookup is. Currently, the only valid entry is "small". * @param source The source of delay model. Currently, the only valid entry is "text". * @param fileName The text file describing the delay model. - * @throws IllegalArgumentException This method throw IllegalArgumentException if the fileName + * @throws IllegalArgumentException This method throws IllegalArgumentException if the fileName * does not exist. */ - private static void newDelayModel(String mode, String source, String fileName) { + private static DelayModel newDelayModel(String mode, String source, String fileName) { DelayModelSource src; if (source.equalsIgnoreCase(valid_source[0])) { src = new DelayModelSourceFromText(fileName); @@ -97,7 +95,7 @@ private static void newDelayModel(String mode, String source, String fileName) { } if (mode.equalsIgnoreCase(valid_mode[0])) { - aModel = new SmallDelayModel(src); + return new SmallDelayModel(src); } else { throw new IllegalArgumentException("DelayModelBuilder: Unknown mode to newDelayModel."); } diff --git a/src/com/xilinx/rapidwright/timing/NullDelayModel.java b/src/com/xilinx/rapidwright/timing/NullDelayModel.java new file mode 100644 index 000000000..7f22fdc66 --- /dev/null +++ b/src/com/xilinx/rapidwright/timing/NullDelayModel.java @@ -0,0 +1,60 @@ +/* + * Copyright (c) 2026, Advanced Micro Devices, Inc. + * All rights reserved. + * + * This file is part of RapidWright. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.xilinx.rapidwright.timing; + +import com.xilinx.rapidwright.device.SiteTypeEnum; + +/** + * Topology-only fallback DelayModel used when no per-series delay data ships + * for the device (e.g., Versal). Returns zero for every lookup so that + * TimingGraph can still build the structural graph; callers are expected to + * overwrite delays via TimingEdge.set*Delay(...) if accurate timing is needed. + */ +class NullDelayModel implements DelayModel { + + static final NullDelayModel INSTANCE = new NullDelayModel(); + + private NullDelayModel() {} + + @Override + public Short getIntraSiteDelay(SiteTypeEnum siteTypeName, String frBelPin, String toBelPin) { + return 0; + } + + @Override + public short getLogicDelay(short belIdx, String frBelPin, String toBelPin, int encodedConfig) { + return 0; + } + + @Override + public short getLogicDelay(short belIdx, String frBelPin, String toBelPin) { + return 0; + } + + @Override + public int getEncodedConfigCode(String value) { + return 0; + } + + @Override + public short getBELIndex(String belName) { + return 0; + } +} diff --git a/src/com/xilinx/rapidwright/timing/TimingGraph.java b/src/com/xilinx/rapidwright/timing/TimingGraph.java index 06f4da50c..e84d8f126 100644 --- a/src/com/xilinx/rapidwright/timing/TimingGraph.java +++ b/src/com/xilinx/rapidwright/timing/TimingGraph.java @@ -37,6 +37,7 @@ import java.util.Set; import org.jgrapht.GraphPath; +import org.jgrapht.alg.cycle.CycleDetector; import org.jgrapht.alg.shortestpath.AllDirectedPaths; import org.jgrapht.alg.shortestpath.BellmanFordShortestPath; import org.jgrapht.alg.shortestpath.KShortestSimplePaths; @@ -268,6 +269,87 @@ public void setTimingRequirement(float requirement) { computeArrivalTimes(); } + /** + * Detects cycles in the timing graph and removes one back-edge per cycle + * until the graph is acyclic. + * + * Combinational cycles in the timing graph can arise from latch feedback, + * intentional combinational loops that Vivado disables via + * {@code set_disable_timing}, internal DSP/BRAM feedback the lightweight + * model does not know about, or LUT-output-fed-back-to-LUT-input patterns + * through routethrus. Because {@link #setOrderedTimingVertexLists()} and + * the topological-order arrival computation require a DAG, any such cycle + * crashes path enumeration. Breaking back-edges here is the analog of + * Vivado's automatic arc disabling. + * + * Each removal is reported on {@code System.err} when {@link #verbose} is + * true so the broken arcs can be inspected. + * + * Termination: each iteration either exits or removes exactly one edge, + * so the loop runs at most {@code edgeSet().size()} times. As a defensive + * guard against future changes that might violate that invariant (a + * removeEdge that silently fails, or a mutation of the graph from another + * thread), the loop also enforces an explicit iteration cap based on the + * initial edge count and throws {@link IllegalStateException} if the cap + * is hit or {@code removeEdge} fails to shrink the graph. + * + * @return The number of edges removed. + */ + public int breakCycles() { + int removed = 0; + // Initial edge count gives a hard upper bound on useful iterations: + // we cannot remove more edges than exist, and each successful pass + // removes exactly one. + final int maxIters = edgeSet().size(); + for (int i = 0; i <= maxIters; i++) { + CycleDetector detector = new CycleDetector<>(this); + if (!detector.detectCycles()) { + return removed; + } + Set inCycle = detector.findCycles(); + if (inCycle.isEmpty()) { + return removed; + } + TimingVertex v = inCycle.iterator().next(); + TimingEdge backEdge = null; + for (TimingEdge e : outgoingEdgesOf(v)) { + if (inCycle.contains(e.getDst())) { + backEdge = e; + break; + } + } + if (backEdge == null) { + // Should be unreachable: a vertex returned by findCycles() must + // have an outgoing edge to another in-cycle vertex. + throw new IllegalStateException( + "breakCycles: vertex " + v.getName() + " was reported " + + "as participating in a cycle but has no " + + "outgoing edge to another in-cycle vertex"); + } + if (verbose) { + System.err.println("[TimingGraph] breaking cycle by removing " + + backEdge.getSrc().getName() + " -> " + + backEdge.getDst().getName()); + } + int beforeEdges = edgeSet().size(); + removeEdge(backEdge); + if (edgeSet().size() != beforeEdges - 1) { + // removeEdge claimed to act but the graph didn't shrink: + // without monotonic progress the loop could spin forever. + throw new IllegalStateException( + "breakCycles: removeEdge did not shrink the graph " + + "(before=" + beforeEdges + + ", after=" + edgeSet().size() + ")"); + } + removed++; + } + // Iteration cap reached. The bound is exactly the initial edge count, + // so reaching it means progress is no longer monotonic. + throw new IllegalStateException( + "breakCycles: exceeded iteration cap (" + maxIters + + "); graph may still contain cycles"); + } + /** * Creates and Sets the lists of ordered TimingVertices */ @@ -1756,18 +1838,28 @@ public int addNetDelayEdges(Net net) { if (haveIntrasiteNet) {//LUT driving a FF is here String param2 = srcCell.getBELName()+"/"+ source.getName(); String param3 = null; - if (sink_belpins.get(D) == null) { - param3 = dstCell.getBELName() +"/" + stringSinks.get(D).getName(); - } else { - param3 = dstCell.getBELName() +"/" +sink_belpins.get(D).getName(); + // Prefer the BEL pin; fall back to the SitePinInst name; if + // neither resolved (some Versal sinks have neither a physical + // pin mapping nor a SitePinInst), leave param3 null so we skip + // the lookup and use 0 as the delay -- the edge is still added. + if (sink_belpins.get(D) != null) { + param3 = dstCell.getBELName() + "/" + sink_belpins.get(D).getName(); + } else if (stringSinks.get(D) != null) { + param3 = dstCell.getBELName() + "/" + stringSinks.get(D).getName(); } float tmpNetDelay; - Short returnValue = intrasiteAndLogicDelayModel.getIntraSiteDelay( - si.getSiteTypeEnum(), - param2, - param3); + Short returnValue = (param3 == null) + ? null + : intrasiteAndLogicDelayModel.getIntraSiteDelay( + si.getSiteTypeEnum(), + param2, + param3); if (returnValue == null) { - continue; + // Unknown intra-site delay (e.g., Versal site type not in the + // UltraScale+ model, or unresolvable sink pin). Keep the + // edge so the structural graph is preserved; delay will be 0 + // and can be overlaid later. + returnValue = 0; } tmpNetDelay = (float) returnValue; @@ -1783,11 +1875,23 @@ public int addNetDelayEdges(Net net) { if (local_spi_source == null || spi_sink == null) { if (local_spi_source == null && spi_sink == null) {//source and sink are null String param2 = srcCell.getBELName()+"/"+ source.getName(); - String param3 = dstCell.getBELName() +"/" +sink_belpins.get(D).getName(); - float tmpNetDelay = intrasiteAndLogicDelayModel.getIntraSiteDelay( - si.getSiteTypeEnum(), - param2, - param3); + // Same fallback as the haveIntrasiteNet branch: prefer + // the BEL pin name, then the SitePinInst name; if + // neither resolved, treat the delay as 0 and keep the + // edge for topology. + String param3 = null; + if (sink_belpins.get(D) != null) { + param3 = dstCell.getBELName() + "/" + sink_belpins.get(D).getName(); + } else if (stringSinks.get(D) != null) { + param3 = dstCell.getBELName() + "/" + stringSinks.get(D).getName(); + } + Short rv = (param3 == null) + ? null + : intrasiteAndLogicDelayModel.getIntraSiteDelay( + si.getSiteTypeEnum(), + param2, + param3); + float tmpNetDelay = (rv == null) ? 0f : (float) rv; netDelay = tmpNetDelay; intraSiteDelay = tmpNetDelay; forceUpdateEdge = true; diff --git a/src/com/xilinx/rapidwright/timing/TimingManager.java b/src/com/xilinx/rapidwright/timing/TimingManager.java index 36d6400b2..4fa95836f 100644 --- a/src/com/xilinx/rapidwright/timing/TimingManager.java +++ b/src/com/xilinx/rapidwright/timing/TimingManager.java @@ -347,6 +347,10 @@ private boolean build(boolean isPartialRouting, Collection targetNets) { private boolean postBuild() { if (routerTimer != null) routerTimer.createRuntimeTracker("post graph build", "Initialization").start(); timingGraph.removeClockCrossingPaths(); + // setOrderedTimingVertexLists() uses a topological-order iterator which + // throws on cycles. Break any cycles first (analog of Vivado's automatic + // arc disabling for latch feedback / combinational loops / etc.). + timingGraph.breakCycles(); timingGraph.buildSuperGraphPaths(); timingGraph.setOrderedTimingVertexLists(); if (routerTimer != null) routerTimer.getRuntimeTracker("post graph build").stop(); diff --git a/src/com/xilinx/rapidwright/timing/TimingModel.java b/src/com/xilinx/rapidwright/timing/TimingModel.java index 0a560fcea..912708b19 100644 --- a/src/com/xilinx/rapidwright/timing/TimingModel.java +++ b/src/com/xilinx/rapidwright/timing/TimingModel.java @@ -172,6 +172,22 @@ public class TimingModel { private Tile[] goodRowTypes; private Device device; + /** + * When true, the model was built for a series that has no shipped delay data + * (e.g., Versal). The graph structure can still be built, but all computed + * delays are 0. Callers are expected to overlay Vivado-reported delays via + * TimingEdge.set*Delay(...) if accurate timing is required. + */ + private boolean topologyOnly = false; + + /** + * @return Whether this model is in topology-only mode (no usable delay data + * was found for the device's series; calcDelay() returns 0). + */ + public boolean isTopologyOnly() { + return topologyOnly; + } + public HashMap> forDebugTimingGroupByPorts; private static final HashSet ultraScaleFlopNames; @@ -240,11 +256,25 @@ public void build() { String series = device.getSeries().name().toLowerCase(); String fileName = TimingModel.TIMING_DATA_DIR + File.separator + series + File.separator + "intersite_delay_terms.txt"; - if (!readDelayTerms(fileName)) { - throw new RuntimeException("Error reading file:" + fileName); + File dataFile = new File(FileTools.getRapidWrightPath() + File.separator + fileName); + if (dataFile.exists()) { + if (!readDelayTerms(fileName)) { + throw new RuntimeException("Error reading file:" + fileName); + } + } else { + // No intersite delay data for this series (e.g., Versal): fall back to + // topology-only mode. Default coefficients remain in place; calcDelay() + // will short-circuit to 0. + topologyOnly = true; } intrasiteAndLogicDelayModel = DelayModelBuilder.getDelayModel(series); + if (topologyOnly) { + // Skip the UltraScale+-specific tile-row scan and distance table build. + // Structural graph construction in TimingGraph does not need them. + return; + } + // create a good row for netDelay model, in terms of capturing resource types within a row Tile[][] tiles = device.getTiles(); goodRowTypes = new Tile[tiles[1].length]; @@ -309,6 +339,13 @@ public float calcDelay(SitePinInst startPinInst, SitePinInst endPinInst, Net net */ public float calcDelay(SitePinInst startPinInst, SitePinInst endPinInst, BELPin sourceBELPin, BELPin sinkBELPin, Net net) { + if (topologyOnly) { + // Skip PIP/intent-code traversal that assumes UltraScale+ semantics. + // The TimingEdge will still be created with correct source/sink and + // physical net annotations; the delay value is just 0. + intrasiteDelay = 0f; + return 0f; + } ArrayList intentCodes = new ArrayList<>(); HashMap pipTypes = new LinkedHashMap<>(); @@ -496,8 +533,13 @@ protected boolean readDelayTerms(String filename) { // Compute before reading from file to allow overriding. Tile tile = findReferenceTile(); - START_TILE_COL = tile.getColumn(); - START_TILE_ROW = tile.getRow(); + if (tile != null) { + START_TILE_COL = tile.getColumn(); + START_TILE_ROW = tile.getRow(); + } + // If null (e.g., Versal: no UltraScale+ CLB-flanked INT tile), keep + // the default START_TILE_ROW/COL. Values overridden via the file below + // still take precedence. boolean result = true; try (BufferedReader br = new BufferedReader(new FileReader(FileTools.getRapidWrightPath() + File.separator + filename))) { diff --git a/test/src/com/xilinx/rapidwright/timing/TestTimingGraph.java b/test/src/com/xilinx/rapidwright/timing/TestTimingGraph.java index e04329f47..47f8204a0 100644 --- a/test/src/com/xilinx/rapidwright/timing/TestTimingGraph.java +++ b/test/src/com/xilinx/rapidwright/timing/TestTimingGraph.java @@ -22,11 +22,16 @@ package com.xilinx.rapidwright.timing; +import java.util.List; +import java.util.NoSuchElementException; + import org.jgrapht.GraphPath; +import org.jgrapht.alg.cycle.CycleDetector; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; import com.xilinx.rapidwright.design.Design; +import com.xilinx.rapidwright.device.Series; import com.xilinx.rapidwright.edif.EDIFHierNet; import com.xilinx.rapidwright.support.RapidWrightDCP; @@ -59,4 +64,173 @@ public void testGetTimingPaths() { Assertions.assertEquals(1611.1f, tg.getPathDelay(otherPath)); } + + /** + * Verifies that a TimingGraph can be built for a Versal design in + * topology-only mode. The lightweight delay model has no Versal data, + * so all delays are 0; the structural graph (vertices, edges with + * Net/SitePinInst annotations, and Q->D paths between FDRE/FDSE/FDPE/FDCE + * cells) must still be produced. + */ + @Test + public void testVersalTopologyOnly() { + Design d = RapidWrightDCP.loadDCP("picoblaze_2022.2.dcp"); + Assertions.assertEquals(Series.Versal, d.getDevice().getSeries(), + "picoblaze_2022.2.dcp is expected to be a Versal design"); + + TimingManager tm = new TimingManager(d); + Assertions.assertTrue(tm.getTimingModel().isTopologyOnly(), + "TimingModel should fall back to topology-only mode on Versal"); + + TimingGraph tg = tm.getTimingGraph(); + Assertions.assertTrue(tg.vertexSet().size() > 0, "graph has no vertices"); + Assertions.assertTrue(tg.edgeSet().size() > 0, "graph has no edges"); + + // At least one edge should carry a physical Net annotation (proves the + // structural information survives even with no delay data). + long edgesWithNet = tg.edgeSet().stream() + .filter(e -> e.getNet() != null) + .count(); + Assertions.assertTrue(edgesWithNet > 0, + "no TimingEdges carry a physical Net annotation"); + + // FF vertices should be marked from FDRE/FDSE/FDPE/FDCE cells in the design. + long flopOutputs = tg.vertexSet().stream().filter(TimingVertex::getFlopOutput).count(); + long flopInputs = tg.vertexSet().stream().filter(TimingVertex::getFlopInput ).count(); + Assertions.assertTrue(flopOutputs > 0, "no FF output vertices were marked"); + Assertions.assertTrue(flopInputs > 0, "no FF input vertices were marked"); + + // Path enumeration: getMaxDelayPath() internally calls buildGraphPaths(1) + // (K-shortest-paths through super-source/super-sink), which is the working + // entry point for path enumeration. With all delays = 0, the returned path + // is still a structurally valid super-source -> super-sink walk. + GraphPath path = tg.getMaxDelayPath(); + Assertions.assertNotNull(path, "getMaxDelayPath() returned null"); + Assertions.assertTrue(path.getEdgeList().size() > 0, + "max-delay path has no edges"); + + // The internal cache populated by getMaxDelayPath() should contain >= 1 path. + Assertions.assertTrue(tg.getGraphPaths().size() > 0, + "graph path cache is empty after getMaxDelayPath()"); + + // Walking the path's edges, at least one interior edge should carry a Net. + long pathEdgesWithNet = path.getEdgeList().stream() + .filter(e -> e.getNet() != null) + .count(); + Assertions.assertTrue(pathEdgesWithNet > 0, + "enumerated path has no edges with Net annotation"); + } + + /** + * Verifies that TimingGraph.breakCycles() recovers from a non-DAG state. + * + * Combinational cycles can show up in real designs (latch feedback, + * intentional combinational loops, internal DSP/BRAM feedback the + * lightweight model doesn't know about). Without a loop breaker, + * setOrderedTimingVertexLists()'s topological-order iterator throws + * IllegalArgumentException("Graph is not a DAG"). + * + * The test loads picoblaze_2022.2.dcp (Versal, so the graph itself is built + * in topology-only mode), injects an artificial back-edge to simulate the + * cycle pattern, asserts the cycle is detected, runs breakCycles(), and + * asserts the graph is acyclic and topological order succeeds. + */ + @Test + public void testBreakCyclesRecoversFromInjectedCycle() { + Design d = RapidWrightDCP.loadDCP("picoblaze_2022.2.dcp"); + TimingManager tm = new TimingManager(d); + TimingGraph tg = tm.getTimingGraph(); + + // postBuild() already ran breakCycles() once; the graph should be a DAG. + Assertions.assertFalse(new CycleDetector<>(tg).detectCycles(), + "graph should be acyclic after initial postBuild"); + + // Pick an interior edge (not super-source/sink) and add the reverse + // edge to introduce a 2-cycle. + TimingEdge sample = tg.edgeSet().stream() + .filter(e -> e.getSrc() != null && e.getDst() != null) + .filter(e -> !"superSource".equals(e.getSrc().getName())) + .filter(e -> !"superSink".equals(e.getDst().getName())) + .findFirst() + .orElseThrow(() -> new NoSuchElementException( + "no interior edge found to seed the cycle")); + TimingVertex a = sample.getSrc(); + TimingVertex b = sample.getDst(); + Assertions.assertNull(tg.getEdge(b, a), + "test precondition: reverse edge should not already exist"); + TimingEdge backEdge = new TimingEdge(tg, b, a); + Assertions.assertTrue(tg.addEdge(b, a, backEdge), + "failed to inject reverse edge"); + Assertions.assertTrue(new CycleDetector<>(tg).detectCycles(), + "injected back-edge should have introduced a cycle"); + + // Without breakCycles(), setOrderedTimingVertexLists() throws. + Assertions.assertThrows(IllegalArgumentException.class, + () -> tg.setOrderedTimingVertexLists(), + "topological-order iterator should throw on a non-DAG"); + + // breakCycles() should remove at least the back-edge we added and + // leave the graph acyclic. + int removed = tg.breakCycles(); + Assertions.assertTrue(removed >= 1, + "breakCycles() should have removed at least one edge"); + Assertions.assertFalse(new CycleDetector<>(tg).detectCycles(), + "graph should be acyclic after breakCycles()"); + + // Topological order should now succeed. + Assertions.assertDoesNotThrow(tg::setOrderedTimingVertexLists, + "setOrderedTimingVertexLists() should succeed after breakCycles()"); + } + + /** + * Stress-test breakCycles() with many injected cycles. + * + * Injects N independent back-edges into the picoblaze Versal graph + * (creating N separate 2-cycles), then verifies that: + * - breakCycles() terminates, + * - the number of edges removed is in the range [N, edgeCountBefore], + * - the final edge count equals (edgeCountBefore - removed), + * - the graph is acyclic afterwards. + * + * This pins down the termination bound: each successful iteration must + * strictly shrink the edge count, so the loop can run at most + * edgeSet().size() times no matter how many cycles are present. + */ + @Test + public void testBreakCyclesTerminatesUnderManyInjectedCycles() { + Design d = RapidWrightDCP.loadDCP("picoblaze_2022.2.dcp"); + TimingManager tm = new TimingManager(d); + TimingGraph tg = tm.getTimingGraph(); + + final int target = 50; + int injected = 0; + for (TimingEdge e : new java.util.ArrayList<>(tg.edgeSet())) { + if (injected >= target) break; + TimingVertex a = e.getSrc(); + TimingVertex b = e.getDst(); + if (a == null || b == null) continue; + if ("superSource".equals(a.getName())) continue; + if ("superSink".equals(b.getName())) continue; + if (tg.getEdge(b, a) != null) continue; // skip if reverse already exists + tg.addEdge(b, a, new TimingEdge(tg, b, a)); + injected++; + } + Assertions.assertEquals(target, injected, + "could not inject the requested number of back-edges"); + Assertions.assertTrue(new CycleDetector<>(tg).detectCycles(), + "expected cycles after injection"); + + int edgesBefore = tg.edgeSet().size(); + int removed = tg.breakCycles(); + + Assertions.assertTrue(removed >= injected, + "breakCycles should remove at least one edge per injected cycle" + + "; injected=" + injected + " removed=" + removed); + Assertions.assertTrue(removed <= edgesBefore, + "breakCycles cannot remove more edges than existed"); + Assertions.assertEquals(edgesBefore - removed, tg.edgeSet().size(), + "edge count decrease must match the reported removal count"); + Assertions.assertFalse(new CycleDetector<>(tg).detectCycles(), + "graph should be acyclic after breakCycles()"); + } }