physdes-py 0.8__tar.gz → 0.9__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (251) hide show
  1. {physdes_py-0.8 → physdes_py-0.9}/CHANGELOG.md +18 -0
  2. {physdes_py-0.8/src/physdes_py.egg-info → physdes_py-0.9}/PKG-INFO +1 -2
  3. {physdes_py-0.8 → physdes_py-0.9}/alpha10.svg +1 -1
  4. {physdes_py-0.8 → physdes_py-0.9}/alpha12.svg +1 -1
  5. {physdes_py-0.8 → physdes_py-0.9}/alpha14.svg +1 -1
  6. {physdes_py-0.8 → physdes_py-0.9}/alpha8.svg +1 -1
  7. {physdes_py-0.8 → physdes_py-0.9}/alpha9998.svg +1 -1
  8. {physdes_py-0.8 → physdes_py-0.9}/example_route3d_with_constraint_and_keepouts.svg +1 -1
  9. {physdes_py-0.8 → physdes_py-0.9}/example_route3d_with_steiner_and_keepouts.svg +1 -1
  10. {physdes_py-0.8 → physdes_py-0.9}/example_route_with_delay_constraint.svg +1 -1
  11. {physdes_py-0.8 → physdes_py-0.9}/example_route_with_keepouts.svg +1 -1
  12. {physdes_py-0.8 → physdes_py-0.9}/example_route_with_steiner.svg +1 -1
  13. {physdes_py-0.8 → physdes_py-0.9}/example_route_with_steiner_and_keepouts.svg +1 -1
  14. {physdes_py-0.8 → physdes_py-0.9}/example_route_with_steiner_special.svg +1 -1
  15. {physdes_py-0.8 → physdes_py-0.9}/neg_specical10.svg +1 -1
  16. {physdes_py-0.8 → physdes_py-0.9}/requirements/default.txt +0 -1
  17. {physdes_py-0.8 → physdes_py-0.9}/setup.cfg +0 -1
  18. {physdes_py-0.8 → physdes_py-0.9}/src/physdes/cts/dme_algorithm.py +17 -15
  19. {physdes_py-0.8 → physdes_py-0.9}/src/physdes/manhattan_arc.py +0 -5
  20. {physdes_py-0.8 → physdes_py-0.9}/src/physdes/router/routing_tree.py +94 -34
  21. {physdes_py-0.8 → physdes_py-0.9}/src/physdes/steiner_forest/congestion_map.py +9 -8
  22. {physdes_py-0.8 → physdes_py-0.9}/src/physdes/steiner_forest/steiner_forest_grid.py +66 -56
  23. {physdes_py-0.8 → physdes_py-0.9/src/physdes_py.egg-info}/PKG-INFO +1 -2
  24. {physdes_py-0.8 → physdes_py-0.9}/src/physdes_py.egg-info/SOURCES.txt +1 -0
  25. {physdes_py-0.8 → physdes_py-0.9}/src/physdes_py.egg-info/requires.txt +0 -1
  26. {physdes_py-0.8 → physdes_py-0.9}/src/physdes_py.egg-info/scm_file_list.json +1 -0
  27. physdes_py-0.9/src/physdes_py.egg-info/scm_version.json +8 -0
  28. {physdes_py-0.8 → physdes_py-0.9}/tests/conftest.py +13 -0
  29. physdes_py-0.9/tests/test_benchmarks.py +126 -0
  30. physdes_py-0.8/src/physdes_py.egg-info/scm_version.json +0 -8
  31. {physdes_py-0.8 → physdes_py-0.9}/.coveragerc +0 -0
  32. {physdes_py-0.8 → physdes_py-0.9}/.flake8 +0 -0
  33. {physdes_py-0.8 → physdes_py-0.9}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
  34. {physdes_py-0.8 → physdes_py-0.9}/.github/ISSUE_TEMPLATE/documentation.md +0 -0
  35. {physdes_py-0.8 → physdes_py-0.9}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
  36. {physdes_py-0.8 → physdes_py-0.9}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  37. {physdes_py-0.8 → physdes_py-0.9}/.github/workflows/jekyll-gh-pages.yml +0 -0
  38. {physdes_py-0.8 → physdes_py-0.9}/.github/workflows/multi-platforms.yml +0 -0
  39. {physdes_py-0.8 → physdes_py-0.9}/.github/workflows/python-app.yml +0 -0
  40. {physdes_py-0.8 → physdes_py-0.9}/.github/workflows/python-publish.yml +0 -0
  41. {physdes_py-0.8 → physdes_py-0.9}/.gitignore +0 -0
  42. {physdes_py-0.8 → physdes_py-0.9}/.gitpod.Dockerfile +0 -0
  43. {physdes_py-0.8 → physdes_py-0.9}/.gitpod.yml +0 -0
  44. {physdes_py-0.8 → physdes_py-0.9}/.hadolint.yaml +0 -0
  45. {physdes_py-0.8 → physdes_py-0.9}/.isort.cfg +0 -0
  46. {physdes_py-0.8 → physdes_py-0.9}/.markdownlint.yaml +0 -0
  47. {physdes_py-0.8 → physdes_py-0.9}/.pre-commit-config.yaml +0 -0
  48. {physdes_py-0.8 → physdes_py-0.9}/.readthedocs.yml +0 -0
  49. {physdes_py-0.8 → physdes_py-0.9}/.shellcheckrc +0 -0
  50. {physdes_py-0.8 → physdes_py-0.9}/AGENTS.md +0 -0
  51. {physdes_py-0.8 → physdes_py-0.9}/AUTHORS.rst +0 -0
  52. {physdes_py-0.8 → physdes_py-0.9}/CHANGELOG.rst +0 -0
  53. {physdes_py-0.8 → physdes_py-0.9}/CONTRIBUTING.rst +0 -0
  54. {physdes_py-0.8 → physdes_py-0.9}/GEMINI.md +0 -0
  55. {physdes_py-0.8 → physdes_py-0.9}/LICENSE +0 -0
  56. {physdes_py-0.8 → physdes_py-0.9}/README.md +0 -0
  57. {physdes_py-0.8 → physdes_py-0.9}/_config.yml +0 -0
  58. {physdes_py-0.8 → physdes_py-0.9}/changelog/1.misc.rst +0 -0
  59. {physdes_py-0.8 → physdes_py-0.9}/changelog/README.md +0 -0
  60. {physdes_py-0.8 → physdes_py-0.9}/coverage.json +0 -0
  61. {physdes_py-0.8 → physdes_py-0.9}/demo_overlap.py +0 -0
  62. {physdes_py-0.8 → physdes_py-0.9}/demo_overlap.svg +0 -0
  63. {physdes_py-0.8 → physdes_py-0.9}/diagram.bob +0 -0
  64. {physdes_py-0.8 → physdes_py-0.9}/dme_algorithm-slides.html +0 -0
  65. {physdes_py-0.8 → physdes_py-0.9}/dme_algorithm-slides.md +0 -0
  66. {physdes_py-0.8 → physdes_py-0.9}/docs/Makefile +0 -0
  67. {physdes_py-0.8 → physdes_py-0.9}/docs/_static/.gitignore +0 -0
  68. {physdes_py-0.8 → physdes_py-0.9}/docs/authors.rst +0 -0
  69. {physdes_py-0.8 → physdes_py-0.9}/docs/build_from_md.py +0 -0
  70. {physdes_py-0.8 → physdes_py-0.9}/docs/build_remark.py +0 -0
  71. {physdes_py-0.8 → physdes_py-0.9}/docs/build_slides.py +0 -0
  72. {physdes_py-0.8 → physdes_py-0.9}/docs/changelog.rst +0 -0
  73. {physdes_py-0.8 → physdes_py-0.9}/docs/conf.py +0 -0
  74. {physdes_py-0.8 → physdes_py-0.9}/docs/contributing.md +0 -0
  75. {physdes_py-0.8 → physdes_py-0.9}/docs/contributing.rst +0 -0
  76. {physdes_py-0.8 → physdes_py-0.9}/docs/essays/essay.md +0 -0
  77. {physdes_py-0.8 → physdes_py-0.9}/docs/essays/polygon_vs_rpolygon_comparison.html +0 -0
  78. {physdes_py-0.8 → physdes_py-0.9}/docs/essays/polygon_vs_rpolygon_comparison.md +0 -0
  79. {physdes_py-0.8 → physdes_py-0.9}/docs/examples/plot_cts.py +0 -0
  80. {physdes_py-0.8 → physdes_py-0.9}/docs/examples/plot_global_router.py +0 -0
  81. {physdes_py-0.8 → physdes_py-0.9}/docs/examples/plot_rectangles.py +0 -0
  82. {physdes_py-0.8 → physdes_py-0.9}/docs/examples/plot_rpolygon.py +0 -0
  83. {physdes_py-0.8 → physdes_py-0.9}/docs/figures_demo.rst +0 -0
  84. {physdes_py-0.8 → physdes_py-0.9}/docs/index.rst +0 -0
  85. {physdes_py-0.8 → physdes_py-0.9}/docs/license.rst +0 -0
  86. {physdes_py-0.8 → physdes_py-0.9}/docs/misc/note.md +0 -0
  87. {physdes_py-0.8 → physdes_py-0.9}/docs/misc/report.md +0 -0
  88. {physdes_py-0.8 → physdes_py-0.9}/docs/my_congestion.svg +0 -0
  89. {physdes_py-0.8 → physdes_py-0.9}/docs/proposals/fyproj2026.md +0 -0
  90. {physdes_py-0.8 → physdes_py-0.9}/docs/proposals/fyproj2026.md.bad +0 -0
  91. {physdes_py-0.8 → physdes_py-0.9}/docs/proposals/fyproj2026_cts.md +0 -0
  92. {physdes_py-0.8 → physdes_py-0.9}/docs/proposals/fyproj2026_cts_2d.md +0 -0
  93. {physdes_py-0.8 → physdes_py-0.9}/docs/proposals/mulit-fpga-routing.md +0 -0
  94. {physdes_py-0.8 → physdes_py-0.9}/docs/readme.rst +0 -0
  95. {physdes_py-0.8 → physdes_py-0.9}/docs/requirements.txt +0 -0
  96. {physdes_py-0.8 → physdes_py-0.9}/docs/slides_content.md +0 -0
  97. {physdes_py-0.8 → physdes_py-0.9}/docs/steiner_forest_diagonal.svg +0 -0
  98. {physdes_py-0.8 → physdes_py-0.9}/environment.yml +0 -0
  99. {physdes_py-0.8 → physdes_py-0.9}/examples/steiner_forest_grid_example.py +0 -0
  100. {physdes_py-0.8 → physdes_py-0.9}/examples/steiner_forest_grid_large_example.py +0 -0
  101. {physdes_py-0.8 → physdes_py-0.9}/examples/steiner_forest_simple_example.py +0 -0
  102. {physdes_py-0.8 → physdes_py-0.9}/experimental/distance_comparison.png +0 -0
  103. {physdes_py-0.8 → physdes_py-0.9}/experimental/rectilinear_distance_field.png +0 -0
  104. {physdes_py-0.8 → physdes_py-0.9}/experimental/rectilinear_voronoi.ipynb +0 -0
  105. {physdes_py-0.8 → physdes_py-0.9}/experimental/rectilinear_voronoi.py +0 -0
  106. {physdes_py-0.8 → physdes_py-0.9}/experimental/rectilinear_voronoi_diamond.png +0 -0
  107. {physdes_py-0.8 → physdes_py-0.9}/experimental/rectilinear_voronoi_grid.png +0 -0
  108. {physdes_py-0.8 → physdes_py-0.9}/experimental/rectilinear_voronoi_random.png +0 -0
  109. {physdes_py-0.8 → physdes_py-0.9}/experimental/steiner_trees_demo.py +0 -0
  110. {physdes_py-0.8 → physdes_py-0.9}/global_router-slides.html +0 -0
  111. {physdes_py-0.8 → physdes_py-0.9}/global_router-slides.md +0 -0
  112. {physdes_py-0.8 → physdes_py-0.9}/my_congestion.svg +0 -0
  113. {physdes_py-0.8 → physdes_py-0.9}/mypy.ini +0 -0
  114. {physdes_py-0.8 → physdes_py-0.9}/oryx-build-commands.txt +0 -0
  115. {physdes_py-0.8 → physdes_py-0.9}/outputs/comparison.svg +0 -0
  116. {physdes_py-0.8 → physdes_py-0.9}/outputs/convex_hull.svg +0 -0
  117. {physdes_py-0.8 → physdes_py-0.9}/outputs/delay_model_comparison.svg +0 -0
  118. {physdes_py-0.8 → physdes_py-0.9}/outputs/delay_model_comparison3d.svg +0 -0
  119. {physdes_py-0.8 → physdes_py-0.9}/outputs/elmore_model_clock_tree.svg +0 -0
  120. {physdes_py-0.8 → physdes_py-0.9}/outputs/elmore_model_clock_tree3d.svg +0 -0
  121. {physdes_py-0.8 → physdes_py-0.9}/outputs/example_route3d_with_constraint.svg +0 -0
  122. {physdes_py-0.8 → physdes_py-0.9}/outputs/example_route3d_with_constraint_and_keepouts.svg +0 -0
  123. {physdes_py-0.8 → physdes_py-0.9}/outputs/example_route3d_with_steiner.svg +0 -0
  124. {physdes_py-0.8 → physdes_py-0.9}/outputs/example_route3d_with_steiner_and_keepouts.svg +0 -0
  125. {physdes_py-0.8 → physdes_py-0.9}/outputs/example_route_with_constraint.svg +0 -0
  126. {physdes_py-0.8 → physdes_py-0.9}/outputs/example_route_with_constraint_specical.svg +0 -0
  127. {physdes_py-0.8 → physdes_py-0.9}/outputs/example_route_with_delay_constraint.svg +0 -0
  128. {physdes_py-0.8 → physdes_py-0.9}/outputs/example_route_with_keepouts.svg +0 -0
  129. {physdes_py-0.8 → physdes_py-0.9}/outputs/example_route_with_steiner.svg +0 -0
  130. {physdes_py-0.8 → physdes_py-0.9}/outputs/example_route_with_steiner_and_keepouts.svg +0 -0
  131. {physdes_py-0.8 → physdes_py-0.9}/outputs/example_route_with_steiner_special.svg +0 -0
  132. {physdes_py-0.8 → physdes_py-0.9}/outputs/linear_model_clock_tree.svg +0 -0
  133. {physdes_py-0.8 → physdes_py-0.9}/outputs/linear_model_clock_tree3d.svg +0 -0
  134. {physdes_py-0.8 → physdes_py-0.9}/outputs/my_congestion.svg +0 -0
  135. {physdes_py-0.8 → physdes_py-0.9}/outputs/neg_specical10.svg +0 -0
  136. {physdes_py-0.8 → physdes_py-0.9}/outputs/polygon_convex_hull.svg +0 -0
  137. {physdes_py-0.8 → physdes_py-0.9}/outputs/polygon_convex_hull13.svg +0 -0
  138. {physdes_py-0.8 → physdes_py-0.9}/outputs/rectilinear-shapes-for-vlsi-physical-desgin.svg +0 -0
  139. {physdes_py-0.8 → physdes_py-0.9}/outputs/rpolygon_convex_cut.svg +0 -0
  140. {physdes_py-0.8 → physdes_py-0.9}/outputs/rpolygon_cut_explicit.svg +0 -0
  141. {physdes_py-0.8 → physdes_py-0.9}/outputs/rpolyon_convex_hull.svg +0 -0
  142. {physdes_py-0.8 → physdes_py-0.9}/outputs/rpolyon_convex_hull13.svg +0 -0
  143. {physdes_py-0.8 → physdes_py-0.9}/outputs/rpolyon_cut_convex.svg +0 -0
  144. {physdes_py-0.8 → physdes_py-0.9}/outputs/rpolyon_cut_convex2.svg +0 -0
  145. {physdes_py-0.8 → physdes_py-0.9}/outputs/rpolyon_xmono.svg +0 -0
  146. {physdes_py-0.8 → physdes_py-0.9}/outputs/rpolyon_xmono_hull.svg +0 -0
  147. {physdes_py-0.8 → physdes_py-0.9}/outputs/rpolyon_ymono_hull.svg +0 -0
  148. {physdes_py-0.8 → physdes_py-0.9}/outputs/steiner_forest.svg +0 -0
  149. {physdes_py-0.8 → physdes_py-0.9}/outputs/steiner_forest_diagonal.svg +0 -0
  150. {physdes_py-0.8 → physdes_py-0.9}/outputs/test.svg +0 -0
  151. {physdes_py-0.8 → physdes_py-0.9}/outputs/xmono_hull.svg +0 -0
  152. {physdes_py-0.8 → physdes_py-0.9}/plot_alpha.py +0 -0
  153. {physdes_py-0.8 → physdes_py-0.9}/presentation_overlap_detection.html +0 -0
  154. {physdes_py-0.8 → physdes_py-0.9}/presentation_overlap_detection.md +0 -0
  155. {physdes_py-0.8 → physdes_py-0.9}/pyproject.toml +0 -0
  156. {physdes_py-0.8 → physdes_py-0.9}/requirements/README.md +0 -0
  157. {physdes_py-0.8 → physdes_py-0.9}/requirements/dev.txt +0 -0
  158. {physdes_py-0.8 → physdes_py-0.9}/requirements/doc.txt +0 -0
  159. {physdes_py-0.8 → physdes_py-0.9}/requirements/extras.txt +0 -0
  160. {physdes_py-0.8 → physdes_py-0.9}/requirements/test.txt +0 -0
  161. {physdes_py-0.8 → physdes_py-0.9}/requirements.txt +0 -0
  162. {physdes_py-0.8 → physdes_py-0.9}/rpolygon-slides.html +0 -0
  163. {physdes_py-0.8 → physdes_py-0.9}/rpolygon-slides.md +0 -0
  164. {physdes_py-0.8 → physdes_py-0.9}/setup.py +0 -0
  165. {physdes_py-0.8 → physdes_py-0.9}/src/physdes/__init__.py +0 -0
  166. {physdes_py-0.8 → physdes_py-0.9}/src/physdes/cts/clk_tree3d_vis.py +0 -0
  167. {physdes_py-0.8 → physdes_py-0.9}/src/physdes/cts/clk_tree_vis.py +0 -0
  168. {physdes_py-0.8 → physdes_py-0.9}/src/physdes/generic.py +0 -0
  169. {physdes_py-0.8 → physdes_py-0.9}/src/physdes/generic.tpy +0 -0
  170. {physdes_py-0.8 → physdes_py-0.9}/src/physdes/interval.py +0 -0
  171. {physdes_py-0.8 → physdes_py-0.9}/src/physdes/manhattan_arc_3d.py +0 -0
  172. {physdes_py-0.8 → physdes_py-0.9}/src/physdes/manhattan_arc_3d.tpy +0 -0
  173. {physdes_py-0.8 → physdes_py-0.9}/src/physdes/point.py +0 -0
  174. {physdes_py-0.8 → physdes_py-0.9}/src/physdes/point.tpy +0 -0
  175. {physdes_py-0.8 → physdes_py-0.9}/src/physdes/polygon.py +0 -0
  176. {physdes_py-0.8 → physdes_py-0.9}/src/physdes/py.typed +0 -0
  177. {physdes_py-0.8 → physdes_py-0.9}/src/physdes/rdllist.py +0 -0
  178. {physdes_py-0.8 → physdes_py-0.9}/src/physdes/recti.py +0 -0
  179. {physdes_py-0.8 → physdes_py-0.9}/src/physdes/router/global_router.py +0 -0
  180. {physdes_py-0.8 → physdes_py-0.9}/src/physdes/router/global_router3d.tpy +0 -0
  181. {physdes_py-0.8 → physdes_py-0.9}/src/physdes/router/routing_tree3d.tpy +0 -0
  182. {physdes_py-0.8 → physdes_py-0.9}/src/physdes/router/routing_visualizer.py +0 -0
  183. {physdes_py-0.8 → physdes_py-0.9}/src/physdes/router/steiner_forest.md +0 -0
  184. {physdes_py-0.8 → physdes_py-0.9}/src/physdes/rpolygon.py +0 -0
  185. {physdes_py-0.8 → physdes_py-0.9}/src/physdes/rpolygon_cut.py +0 -0
  186. {physdes_py-0.8 → physdes_py-0.9}/src/physdes/steiner_forest/my_congestion.svg +0 -0
  187. {physdes_py-0.8 → physdes_py-0.9}/src/physdes/steiner_forest/steiner_forest.md +0 -0
  188. {physdes_py-0.8 → physdes_py-0.9}/src/physdes/steiner_forest/steiner_forest.svg +0 -0
  189. {physdes_py-0.8 → physdes_py-0.9}/src/physdes/steiner_forest/steiner_forest_diagonal.svg +0 -0
  190. {physdes_py-0.8 → physdes_py-0.9}/src/physdes/to_polygon.py +0 -0
  191. {physdes_py-0.8 → physdes_py-0.9}/src/physdes/vector2.py +0 -0
  192. {physdes_py-0.8 → physdes_py-0.9}/src/physdes/vector2.tpy +0 -0
  193. {physdes_py-0.8 → physdes_py-0.9}/src/physdes_py.egg-info/dependency_links.txt +0 -0
  194. {physdes_py-0.8 → physdes_py-0.9}/src/physdes_py.egg-info/not-zip-safe +0 -0
  195. {physdes_py-0.8 → physdes_py-0.9}/src/physdes_py.egg-info/top_level.txt +0 -0
  196. {physdes_py-0.8 → physdes_py-0.9}/svgo.config.js +0 -0
  197. {physdes_py-0.8 → physdes_py-0.9}/test.svg +0 -0
  198. {physdes_py-0.8 → physdes_py-0.9}/tests/__init__.py +0 -0
  199. {physdes_py-0.8 → physdes_py-0.9}/tests/golden.txt +0 -0
  200. {physdes_py-0.8 → physdes_py-0.9}/tests/mysvg.css +0 -0
  201. {physdes_py-0.8 → physdes_py-0.9}/tests/pnrpoly.svg +0 -0
  202. {physdes_py-0.8 → physdes_py-0.9}/tests/run_failing_tests.py +0 -0
  203. {physdes_py-0.8 → physdes_py-0.9}/tests/test_alpha.py +0 -0
  204. {physdes_py-0.8 → physdes_py-0.9}/tests/test_clk_tree_vis.py +0 -0
  205. {physdes_py-0.8 → physdes_py-0.9}/tests/test_dme_algorithm.py +0 -0
  206. {physdes_py-0.8 → physdes_py-0.9}/tests/test_generic_functions.py +0 -0
  207. {physdes_py-0.8 → physdes_py-0.9}/tests/test_global_router.py +0 -0
  208. {physdes_py-0.8 → physdes_py-0.9}/tests/test_global_router3d.py +0 -0
  209. {physdes_py-0.8 → physdes_py-0.9}/tests/test_global_router3d.tpy +0 -0
  210. {physdes_py-0.8 → physdes_py-0.9}/tests/test_global_router3d_with_keepouts.py +0 -0
  211. {physdes_py-0.8 → physdes_py-0.9}/tests/test_global_router3d_with_keepouts2.py +0 -0
  212. {physdes_py-0.8 → physdes_py-0.9}/tests/test_global_router_logic.py +0 -0
  213. {physdes_py-0.8 → physdes_py-0.9}/tests/test_global_router_logic3d.tpy +0 -0
  214. {physdes_py-0.8 → physdes_py-0.9}/tests/test_global_router_with_keepouts.py +0 -0
  215. {physdes_py-0.8 → physdes_py-0.9}/tests/test_global_router_with_keepouts2.py +0 -0
  216. {physdes_py-0.8 → physdes_py-0.9}/tests/test_interval.py +0 -0
  217. {physdes_py-0.8 → physdes_py-0.9}/tests/test_manhattan_arc_3d.py +0 -0
  218. {physdes_py-0.8 → physdes_py-0.9}/tests/test_manhattan_arc_3d.tpy +0 -0
  219. {physdes_py-0.8 → physdes_py-0.9}/tests/test_merge.py +0 -0
  220. {physdes_py-0.8 → physdes_py-0.9}/tests/test_merge3d.tpy +0 -0
  221. {physdes_py-0.8 → physdes_py-0.9}/tests/test_nearest_point_to.tpy +0 -0
  222. {physdes_py-0.8 → physdes_py-0.9}/tests/test_overlap.py +0 -0
  223. {physdes_py-0.8 → physdes_py-0.9}/tests/test_point.py +0 -0
  224. {physdes_py-0.8 → physdes_py-0.9}/tests/test_point_extended.py +0 -0
  225. {physdes_py-0.8 → physdes_py-0.9}/tests/test_point_hypothesis.py +0 -0
  226. {physdes_py-0.8 → physdes_py-0.9}/tests/test_polygon.py +0 -0
  227. {physdes_py-0.8 → physdes_py-0.9}/tests/test_polygon.svg +0 -0
  228. {physdes_py-0.8 → physdes_py-0.9}/tests/test_polygon_convex_decomposition.py +0 -0
  229. {physdes_py-0.8 → physdes_py-0.9}/tests/test_polygon_hypothesis.py +0 -0
  230. {physdes_py-0.8 → physdes_py-0.9}/tests/test_rdllist.py +0 -0
  231. {physdes_py-0.8 → physdes_py-0.9}/tests/test_rect3d.py +0 -0
  232. {physdes_py-0.8 → physdes_py-0.9}/tests/test_recti.py +0 -0
  233. {physdes_py-0.8 → physdes_py-0.9}/tests/test_recti_hypothesis.py +0 -0
  234. {physdes_py-0.8 → physdes_py-0.9}/tests/test_routing_tree.py +0 -0
  235. {physdes_py-0.8 → physdes_py-0.9}/tests/test_routing_tree3d.tpy +0 -0
  236. {physdes_py-0.8 → physdes_py-0.9}/tests/test_routing_visualizer.py +0 -0
  237. {physdes_py-0.8 → physdes_py-0.9}/tests/test_rpolygon.py +0 -0
  238. {physdes_py-0.8 → physdes_py-0.9}/tests/test_rpolygon_convex_cut.py +0 -0
  239. {physdes_py-0.8 → physdes_py-0.9}/tests/test_rpolygon_convex_hull.py +0 -0
  240. {physdes_py-0.8 → physdes_py-0.9}/tests/test_rpolygon_cut.py +0 -0
  241. {physdes_py-0.8 → physdes_py-0.9}/tests/test_steiner_forest_grid.py +0 -0
  242. {physdes_py-0.8 → physdes_py-0.9}/tests/test_to_polygon.py +0 -0
  243. {physdes_py-0.8 → physdes_py-0.9}/tests/test_vector2.py +0 -0
  244. {physdes_py-0.8 → physdes_py-0.9}/tests/test_vector2_hypothesis.py +0 -0
  245. {physdes_py-0.8 → physdes_py-0.9}/tests/xmono_polygon.svg +0 -0
  246. {physdes_py-0.8 → physdes_py-0.9}/tests/xmono_polygon2.svg +0 -0
  247. {physdes_py-0.8 → physdes_py-0.9}/tests/ymono_polygon.svg +0 -0
  248. {physdes_py-0.8 → physdes_py-0.9}/tests/ymono_rpolygon.svg +0 -0
  249. {physdes_py-0.8 → physdes_py-0.9}/tox.ini +0 -0
  250. {physdes_py-0.8 → physdes_py-0.9}/wirelength_vs_alpha.png +0 -0
  251. {physdes_py-0.8 → physdes_py-0.9}/wirelength_vs_alpha_small.png +0 -0
@@ -1,5 +1,23 @@
1
1
  # Changelog
2
2
 
3
+ ## Version 0.9 (2026-10-09)
4
+
5
+ ### Performance
6
+ - **Hot-path allocation & debug-I/O cuts**: `manhattan_arc` drops live `ic()` calls from the DME embedding path (and removes `icecream` from the runtime deps); `congestion_map` guards its demo behind `__main__`; `steiner_forest_grid` uses an iterative `UnionFind.find`, hoists finds 4→2 per edge, indexes `paid` by edge, and replaces O(|F|²) reverse-delete with union-of-paths pruning (~1.7×); `routing_tree` gets a scalar 2D fast path in `_find_insertion_point` and list-join `get_tree_structure` (~4× insertion); `dme_algorithm` precomputes keys and does a single-pass stable partition in `_build_merging_tree` (1.2–1.5×). Output-identical, verified by differential tests. (#10a4e9f)
7
+
8
+ ### Bug Fixes
9
+ - **Unbounded wirelength sentinel**: `insert_terminal_with_steiner` previously ignored the tree's `worst_wirelength` by passing a literal `10**12`; the default is now `float('inf')` and Steiner insertion passes it through, so the unconstrained variant honors any caller-set bound. (#0947827)
10
+
11
+ ### Testing & Code Quality
12
+ - **Temp-cwd isolation for tests**: Added an autouse `_isolate_cwd` fixture so visualization tests' relative-path SVG writes land in pytest's temporary directory instead of the repository root. (#409d422)
13
+ - **New benchmarks**: Added `test_benchmarks.py` (5 benchmarks). (#10a4e9f)
14
+
15
+ ### Code Cleanup
16
+ - **SVG EOF newlines**: Added trailing newlines to the example SVGs and formatted the sources. (#0c698e0)
17
+
18
+ ### Build & CI
19
+ - **Dropped `icecream` runtime dep**: Removed it from `setup.cfg` and `requirements/default.txt`. (#10a4e9f)
20
+
3
21
  ## Version 0.8 (2026-09-04)
4
22
 
5
23
  ### Features
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: physdes-py
3
- Version: 0.8
3
+ Version: 0.9
4
4
  Summary: Physical Design Python Code
5
5
  Home-page: https://github.com/luk036/physdes-py/
6
6
  Author: Wai-Shing Luk
@@ -13,7 +13,6 @@ Classifier: Programming Language :: Python
13
13
  Description-Content-Type: text/markdown; charset=UTF-8
14
14
  Requires-Dist: typing_extensions
15
15
  Requires-Dist: mywheel
16
- Requires-Dist: icecream
17
16
  Provides-Extra: testing
18
17
  Requires-Dist: setuptools; extra == "testing"
19
18
  Requires-Dist: pytest; extra == "testing"
@@ -68,4 +68,4 @@
68
68
  <text x="20" y="140" font-family="Arial" font-size="9">Terminals: 7</text>
69
69
  <text x="20" y="155" font-family="Arial" font-size="9">Steiner: 5</text>
70
70
  <text x="20" y="170" font-family="Arial" font-size="9">Wirelength: 6379.00</text>
71
- </svg>
71
+ </svg>
@@ -64,4 +64,4 @@
64
64
  <text x="20" y="140" font-family="Arial" font-size="9">Terminals: 7</text>
65
65
  <text x="20" y="155" font-family="Arial" font-size="9">Steiner: 4</text>
66
66
  <text x="20" y="170" font-family="Arial" font-size="9">Wirelength: 5867.00</text>
67
- </svg>
67
+ </svg>
@@ -60,4 +60,4 @@
60
60
  <text x="20" y="140" font-family="Arial" font-size="9">Terminals: 7</text>
61
61
  <text x="20" y="155" font-family="Arial" font-size="9">Steiner: 3</text>
62
62
  <text x="20" y="170" font-family="Arial" font-size="9">Wirelength: 5854.00</text>
63
- </svg>
63
+ </svg>
@@ -64,4 +64,4 @@
64
64
  <text x="20" y="140" font-family="Arial" font-size="9">Terminals: 7</text>
65
65
  <text x="20" y="155" font-family="Arial" font-size="9">Steiner: 4</text>
66
66
  <text x="20" y="170" font-family="Arial" font-size="9">Wirelength: 6928.00</text>
67
- </svg>
67
+ </svg>
@@ -68,4 +68,4 @@
68
68
  <text x="20" y="140" font-family="Arial" font-size="9">Terminals: 7</text>
69
69
  <text x="20" y="155" font-family="Arial" font-size="9">Steiner: 5</text>
70
70
  <text x="20" y="170" font-family="Arial" font-size="9">Wirelength: 6379.00</text>
71
- </svg>
71
+ </svg>
@@ -62,4 +62,4 @@
62
62
  <text x="20" y="140" font-family="Arial" font-size="9">Terminals: 7</text>
63
63
  <text x="20" y="155" font-family="Arial" font-size="9">Steiner: 3</text>
64
64
  <text x="20" y="170" font-family="Arial" font-size="9">Wirelength: 7239.00</text>
65
- </svg>
65
+ </svg>
@@ -62,4 +62,4 @@
62
62
  <text x="20" y="140" font-family="Arial" font-size="9">Terminals: 7</text>
63
63
  <text x="20" y="155" font-family="Arial" font-size="9">Steiner: 3</text>
64
64
  <text x="20" y="170" font-family="Arial" font-size="9">Wirelength: 7239.00</text>
65
- </svg>
65
+ </svg>
@@ -68,4 +68,4 @@
68
68
  <text x="20" y="140" font-family="Arial" font-size="9">Terminals: 7</text>
69
69
  <text x="20" y="155" font-family="Arial" font-size="9">Steiner: 5</text>
70
70
  <text x="20" y="170" font-family="Arial" font-size="9">Wirelength: 6379.00</text>
71
- </svg>
71
+ </svg>
@@ -63,4 +63,4 @@
63
63
  <text x="20" y="140" font-family="Arial" font-size="9">Terminals: 7</text>
64
64
  <text x="20" y="155" font-family="Arial" font-size="9">Steiner: 3</text>
65
65
  <text x="20" y="170" font-family="Arial" font-size="9">Wirelength: 6839.00</text>
66
- </svg>
66
+ </svg>
@@ -60,4 +60,4 @@
60
60
  <text x="20" y="140" font-family="Arial" font-size="9">Terminals: 7</text>
61
61
  <text x="20" y="155" font-family="Arial" font-size="9">Steiner: 3</text>
62
62
  <text x="20" y="170" font-family="Arial" font-size="9">Wirelength: 5854.00</text>
63
- </svg>
63
+ </svg>
@@ -63,4 +63,4 @@
63
63
  <text x="20" y="140" font-family="Arial" font-size="9">Terminals: 7</text>
64
64
  <text x="20" y="155" font-family="Arial" font-size="9">Steiner: 3</text>
65
65
  <text x="20" y="170" font-family="Arial" font-size="9">Wirelength: 6839.00</text>
66
- </svg>
66
+ </svg>
@@ -80,4 +80,4 @@
80
80
  <text x="20" y="140" font-family="Arial" font-size="9">Terminals: 11</text>
81
81
  <text x="20" y="155" font-family="Arial" font-size="9">Steiner: 4</text>
82
82
  <text x="20" y="170" font-family="Arial" font-size="9">Wirelength: 36.00</text>
83
- </svg>
83
+ </svg>
@@ -100,4 +100,4 @@
100
100
  <text x="20" y="140" font-family="Arial" font-size="9">Terminals: 11</text>
101
101
  <text x="20" y="155" font-family="Arial" font-size="9">Steiner: 9</text>
102
102
  <text x="20" y="170" font-family="Arial" font-size="9">Wirelength: 45.00</text>
103
- </svg>
103
+ </svg>
@@ -1,4 +1,3 @@
1
1
  decorator>=4.1.0
2
- icecream>=1.0
3
2
  mywheel>=0.2
4
3
  typing_extensions>=4.5
@@ -24,7 +24,6 @@ package_dir =
24
24
  install_requires =
25
25
  typing_extensions
26
26
  mywheel
27
- icecream
28
27
 
29
28
  [options.packages.find]
30
29
  where = src
@@ -386,23 +386,25 @@ class DMEAlgorithm:
386
386
  raise ValueError("Empty node range")
387
387
 
388
388
  mid = lo + count // 2
389
- key_fn = (
390
- (lambda n: n.position.xcoord) if vertical else (lambda n: n.position.ycoord)
391
- )
392
389
 
393
- # Median-partition in O(n): like Rust's select_nth_unstable_by / C++ nth_element
394
390
  sub = nodes[lo:hi]
395
- values = sorted([key_fn(n) for n in sub])
396
- median_val = values[len(values) // 2]
397
- lt = [n for n in sub if key_fn(n) < median_val]
398
- eq = [n for n in sub if key_fn(n) == median_val]
399
- gt = [n for n in sub if key_fn(n) > median_val]
400
- need = count // 2 - len(lt)
401
- if need > 0:
402
- lt.extend(eq[:need])
403
- eq = eq[need:]
404
- gt = eq + gt
405
- nodes[lo:hi] = lt + gt
391
+ keys = (
392
+ [n.position.xcoord for n in sub]
393
+ if vertical
394
+ else [n.position.ycoord for n in sub]
395
+ )
396
+ median_val = sorted(keys)[count // 2]
397
+ lt: List["TreeNode"] = []
398
+ eq: List["TreeNode"] = []
399
+ gt: List["TreeNode"] = []
400
+ for node, value in zip(sub, keys):
401
+ if value < median_val:
402
+ lt.append(node)
403
+ elif value == median_val:
404
+ eq.append(node)
405
+ else:
406
+ gt.append(node)
407
+ nodes[lo:hi] = lt + eq + gt
406
408
 
407
409
  left_child = self._build_merging_tree(nodes, not vertical, lo, mid)
408
410
  right_child = self._build_merging_tree(nodes, not vertical, mid, hi)
@@ -8,8 +8,6 @@ operations during clock tree synthesis.
8
8
 
9
9
  from typing import TYPE_CHECKING, Any, Generic, TypeVar, Union
10
10
 
11
- from icecream import ic # type: ignore
12
-
13
11
  from .generic import min_dist
14
12
  from .interval import enlarge
15
13
  from .point import Point
@@ -274,7 +272,6 @@ class ManhattanArc(Generic[T1, T2]):
274
272
  (700, 550)
275
273
  """
276
274
  nearest_pt = self.impl.nearest_to(manhattan_arc.impl)
277
- ic(nearest_pt)
278
275
  return nearest_pt.inv_rotates()
279
276
 
280
277
  def nearest_point_to(self, other: Point[int, int]) -> Point[Any, Any]:
@@ -295,6 +292,4 @@ class ManhattanArc(Generic[T1, T2]):
295
292
  """
296
293
 
297
294
  ms = ManhattanArc.from_point(other)
298
- ic(self)
299
- ic(ms)
300
295
  return self._nearest_point_to(ms)
@@ -179,7 +179,7 @@ class GlobalRoutingTree:
179
179
  self.nodes: dict[str, RoutingNode] = {}
180
180
  self.next_steiner_id = 1
181
181
  self.next_terminal_id = 1
182
- self.worst_wirelength = 1e100
182
+ self.worst_wirelength = float("inf")
183
183
  self.source = self._create_node(NodeType.Source, source_position)
184
184
 
185
185
  def _create_node(self, node_type: NodeType, pt: Point[Any, Any]) -> "RoutingNode":
@@ -408,7 +408,7 @@ class GlobalRoutingTree:
408
408
  def _find_insertion_point(
409
409
  self,
410
410
  point: Point[Any, Any],
411
- allowed_wirelength: int,
411
+ allowed_wirelength: float,
412
412
  keepouts: Optional[List[Point[Interval[int], Interval[int]]]] = None,
413
413
  ) -> Tuple[Optional["RoutingNode"], "RoutingNode"]:
414
414
  """
@@ -442,7 +442,7 @@ class GlobalRoutingTree:
442
442
  >>> tree = GlobalRoutingTree(Point(0, 0))
443
443
  >>> _ = tree.insert_terminal_node(Point(10, 0))
444
444
  >>> keepouts = [Point(Interval(4, 6), Interval(-1, 1))]
445
- >>> parent, nearest = tree._find_insertion_point(Point(5, 5), 10**12, keepouts)
445
+ >>> parent, nearest = tree._find_insertion_point(Point(5, 5), float("inf"), keepouts)
446
446
  >>> nearest.pt
447
447
  Point(0, 0)
448
448
  """
@@ -454,6 +454,10 @@ class GlobalRoutingTree:
454
454
  min_distance = self.worst_wirelength # initially
455
455
  valid_found = False
456
456
 
457
+ px = point.xcoord
458
+ py = point.ycoord
459
+ scalar_coords = isinstance(px, (int, float)) and isinstance(py, (int, float))
460
+
457
461
  def traverse(node: "RoutingNode") -> None:
458
462
  nonlocal nearest_node
459
463
  nonlocal parent_node
@@ -461,30 +465,82 @@ class GlobalRoutingTree:
461
465
  nonlocal valid_found
462
466
 
463
467
  for child in node.children:
464
- possible_path = node.pt.hull_with(child.pt)
465
- distance = possible_path.min_dist_with(point)
466
- nearest_pt = possible_path.nearest_to(point)
467
-
468
- if keepouts is not None:
469
- block = False
470
- path1 = nearest_pt.hull_with(point)
471
- path2 = nearest_pt.hull_with(node.pt)
472
- path3 = nearest_pt.hull_with(child.pt)
473
- for keepout in keepouts:
474
- if (
475
- keepout.contains(nearest_pt)
476
- or keepout.blocks(path1)
477
- or keepout.blocks(path2)
478
- or keepout.blocks(path3)
479
- ):
480
- block = True
481
- break
482
- if block:
483
- continue
484
-
485
- path_length = (
486
- node.path_length + node.pt.min_dist_with(nearest_pt) + distance
487
- )
468
+ if scalar_coords:
469
+ nx = node.pt.xcoord
470
+ ny = node.pt.ycoord
471
+ cx = child.pt.xcoord
472
+ cy = child.pt.ycoord
473
+ if nx <= cx:
474
+ lo_x, hi_x = nx, cx
475
+ else:
476
+ lo_x, hi_x = cx, nx
477
+ if ny <= cy:
478
+ lo_y, hi_y = ny, cy
479
+ else:
480
+ lo_y, hi_y = cy, ny
481
+ if px < lo_x:
482
+ distance = lo_x - px
483
+ qx = lo_x
484
+ elif px > hi_x:
485
+ distance = px - hi_x
486
+ qx = hi_x
487
+ else:
488
+ distance = 0
489
+ qx = px
490
+ if py < lo_y:
491
+ distance += lo_y - py
492
+ qy = lo_y
493
+ elif py > hi_y:
494
+ distance += py - hi_y
495
+ qy = hi_y
496
+ else:
497
+ qy = py
498
+ to_node = abs(nx - qx) + abs(ny - qy)
499
+ at_node = qx == nx and qy == ny
500
+ at_child = qx == cx and qy == cy
501
+ if keepouts:
502
+ nearest_pt = Point(qx, qy)
503
+ path1 = nearest_pt.hull_with(point)
504
+ path2 = nearest_pt.hull_with(node.pt)
505
+ path3 = nearest_pt.hull_with(child.pt)
506
+ block = False
507
+ for keepout in keepouts:
508
+ if (
509
+ keepout.contains(nearest_pt)
510
+ or keepout.blocks(path1)
511
+ or keepout.blocks(path2)
512
+ or keepout.blocks(path3)
513
+ ):
514
+ block = True
515
+ break
516
+ if block:
517
+ continue
518
+ else:
519
+ possible_path = node.pt.hull_with(child.pt)
520
+ distance = possible_path.min_dist_with(point)
521
+ nearest_pt = possible_path.nearest_to(point)
522
+ to_node = node.pt.min_dist_with(nearest_pt)
523
+ at_node = nearest_pt == node.pt
524
+ at_child = nearest_pt == child.pt
525
+
526
+ if keepouts:
527
+ block = False
528
+ path1 = nearest_pt.hull_with(point)
529
+ path2 = nearest_pt.hull_with(node.pt)
530
+ path3 = nearest_pt.hull_with(child.pt)
531
+ for keepout in keepouts:
532
+ if (
533
+ keepout.contains(nearest_pt)
534
+ or keepout.blocks(path1)
535
+ or keepout.blocks(path2)
536
+ or keepout.blocks(path3)
537
+ ):
538
+ block = True
539
+ break
540
+ if block:
541
+ continue
542
+
543
+ path_length = node.path_length + to_node + distance
488
544
  update = False
489
545
  if path_length <= allowed_wirelength:
490
546
  if valid_found:
@@ -504,10 +560,10 @@ class GlobalRoutingTree:
504
560
 
505
561
  if update:
506
562
  min_distance = distance
507
- if nearest_pt == node.pt:
563
+ if at_node:
508
564
  nearest_node = node
509
565
  parent_node = None
510
- elif nearest_pt == child.pt:
566
+ elif at_child:
511
567
  nearest_node = child
512
568
  parent_node = None
513
569
  else: # need to insert steiner point
@@ -565,7 +621,7 @@ class GlobalRoutingTree:
565
621
  terminal_node = self._create_node(NodeType.Terminal, point)
566
622
 
567
623
  parent_node, nearest_node = self._find_insertion_point(
568
- point, 10**12, keepouts
624
+ point, self.worst_wirelength, keepouts
569
625
  )
570
626
 
571
627
  if parent_node is None:
@@ -732,11 +788,15 @@ class GlobalRoutingTree:
732
788
  if node is None:
733
789
  node = self.source
734
790
 
735
- result = " " * level + str(node) + "\n"
736
- for child in node.children:
737
- result += self.get_tree_structure(child, level + 1)
791
+ parts: List[str] = []
792
+
793
+ def collect(current: "RoutingNode", depth: int) -> None:
794
+ parts.append(" " * depth + str(current) + "\n")
795
+ for child in current.children:
796
+ collect(child, depth + 1)
738
797
 
739
- return result
798
+ collect(node, level)
799
+ return "".join(parts)
740
800
 
741
801
  def find_path_to_source(self, node_id: str) -> List["RoutingNode"]:
742
802
  """Find the path from a node back to the source.
@@ -119,11 +119,12 @@ def draw_congestion_map(
119
119
  print(f"Congestion map saved to {filename}")
120
120
 
121
121
 
122
- grid = [
123
- [0, 20, 40, 80, 100],
124
- [10, 30, 60, 90, 70],
125
- [25, 50, 75, 95, 50],
126
- [0, 15, 35, 55, 30],
127
- ]
128
-
129
- draw_congestion_map(grid, "my_congestion.svg")
122
+ if __name__ == "__main__":
123
+ grid = [
124
+ [0, 20, 40, 80, 100],
125
+ [10, 30, 60, 90, 70],
126
+ [25, 50, 75, 95, 50],
127
+ [0, 15, 35, 55, 30],
128
+ ]
129
+
130
+ draw_congestion_map(grid, "my_congestion.svg")
@@ -43,16 +43,21 @@ class UnionFind:
43
43
  """
44
44
  Find the representative (root) of the set containing idx.
45
45
 
46
- Uses path compression to flatten the tree structure for faster future lookups.
46
+ Uses iterative path compression to flatten the tree structure for
47
+ faster future lookups.
47
48
 
48
49
  :param idx: The index to find.
49
50
  :type idx: int
50
51
  :return: The representative (root) of the set.
51
52
  :rtype: int
52
53
  """
53
- if self.parent[idx] != idx:
54
- self.parent[idx] = self.find(self.parent[idx])
55
- return self.parent[idx]
54
+ parent = self.parent
55
+ root = idx
56
+ while parent[root] != root:
57
+ root = parent[root]
58
+ while parent[idx] != root:
59
+ parent[idx], idx = root, parent[idx]
60
+ return root
56
61
 
57
62
  def union(self, idx1: int, idx2: int) -> bool:
58
63
  """
@@ -208,13 +213,12 @@ def steiner_forest_grid(
208
213
  if row_idx + 1 < height:
209
214
  edges.append((node, node + width, 1.0))
210
215
 
211
- paid: Dict[Tuple[int, int], float] = collections.defaultdict(float)
216
+ paid: List[float] = [0.0] * len(edges)
212
217
  F: List[Tuple[int, int, float]] = [] # list of (u, v, c) added in order
213
218
 
214
219
  while True:
215
- # Compute term_root
216
- term_root: Dict[int, int] = {term: uf.find(term) for term in all_term}
217
- # Check if feasible
220
+ find = uf.find
221
+ term_root: Dict[int, int] = {term: find(term) for term in all_term}
218
222
  feasible = True
219
223
  for source in pair_dict:
220
224
  root_source = term_root[source]
@@ -227,12 +231,10 @@ def steiner_forest_grid(
227
231
  if feasible:
228
232
  break
229
233
 
230
- # Compute comp_terms
231
234
  comp_terms: Dict[int, Set[int]] = collections.defaultdict(set)
232
235
  for terminal in all_term:
233
236
  comp_terms[term_root[terminal]].add(terminal)
234
237
 
235
- # Compute active_comps
236
238
  active_comps: Set[int] = set()
237
239
  for root, terms in comp_terms.items():
238
240
  is_active = False
@@ -246,14 +248,15 @@ def steiner_forest_grid(
246
248
  if is_active:
247
249
  active_comps.add(root)
248
250
 
249
- # Find min_delta and chosen edge(s)
250
251
  min_delta = float("inf")
251
- candidate_es: List[Tuple[int, int, float, Tuple[int, int]]] = []
252
- for node_u, node_v, cost in edges:
253
- if uf.find(node_u) == uf.find(node_v):
252
+ chosen_idx = -1
253
+ chosen_u = chosen_v = 0
254
+ chosen_c = 0.0
255
+ for edge_idx, (node_u, node_v, cost) in enumerate(edges):
256
+ root_u = find(node_u)
257
+ root_v = find(node_v)
258
+ if root_u == root_v:
254
259
  continue
255
- root_u = uf.find(node_u)
256
- root_v = uf.find(node_v)
257
260
  num = 0
258
261
  if root_u in active_comps:
259
262
  num += 1
@@ -261,29 +264,23 @@ def steiner_forest_grid(
261
264
  num += 1
262
265
  if num == 0:
263
266
  continue
264
- key = (min(node_u, node_v), max(node_u, node_v))
265
- paid_val = paid[key]
267
+ paid_val = paid[edge_idx]
266
268
  if paid_val > cost:
267
269
  continue
268
- delta_e = (cost - paid_val) / num if num > 0 else float("inf")
270
+ delta_e = (cost - paid_val) / num
269
271
  if delta_e < min_delta:
270
272
  min_delta = delta_e
271
- candidate_es = [(node_u, node_v, cost, key)]
272
- elif delta_e == min_delta:
273
- candidate_es.append((node_u, node_v, cost, key))
273
+ chosen_idx = edge_idx
274
+ chosen_u, chosen_v, chosen_c = node_u, node_v, cost
274
275
 
275
276
  if min_delta == float("inf"):
276
277
  raise ValueError("Graph is not connected or cannot connect pairs")
277
278
 
278
- # Pick first candidate
279
- chosen_u, chosen_v, chosen_c, chosen_key = candidate_es[0]
280
-
281
- # Update paid for all eligible edges
282
- for u2, v2, c2 in edges:
283
- if uf.find(u2) == uf.find(v2):
279
+ for edge_idx, (u2, v2, c2) in enumerate(edges):
280
+ ru2 = find(u2)
281
+ rv2 = find(v2)
282
+ if ru2 == rv2:
284
283
  continue
285
- ru2 = uf.find(u2)
286
- rv2 = uf.find(v2)
287
284
  num2 = 0
288
285
  if ru2 in active_comps:
289
286
  num2 += 1
@@ -291,39 +288,52 @@ def steiner_forest_grid(
291
288
  num2 += 1
292
289
  if num2 == 0:
293
290
  continue
294
- key2 = (min(u2, v2), max(u2, v2))
295
- paid[key2] += min_delta * num2
296
- if paid[key2] > c2 + 1e-6: # tolerance
297
- paid[key2] = c2
291
+ new_paid = paid[edge_idx] + min_delta * num2
292
+ if new_paid > c2 + 1e-6: # tolerance
293
+ new_paid = c2
294
+ paid[edge_idx] = new_paid
298
295
 
299
- # Add chosen edge if not overpaid
300
- if paid[chosen_key] >= chosen_c - 1e-6:
296
+ if paid[chosen_idx] >= chosen_c - 1e-6:
301
297
  F.append((chosen_u, chosen_v, chosen_c))
302
298
  uf.union(chosen_u, chosen_v)
303
299
 
304
- # Reverse delete
305
- F_pruned: List[Tuple[int, int, float]] = F[:]
306
- for idx in range(len(F) - 1, -1, -1):
307
- temp_uf = UnionFind(n)
308
- for jdx in range(len(F)):
309
- if jdx != idx:
310
- node_u, node_v, _ = F[jdx]
311
- temp_uf.union(node_u, node_v)
312
- connected = True
313
- for source in sources:
314
- for target in pair_dict[source]:
315
- if temp_uf.find(source) != temp_uf.find(target):
316
- connected = False
317
- break
318
- if not connected:
319
- break
320
- if connected:
321
- del F_pruned[idx]
300
+ # ``F`` is a forest: every added edge merges two distinct components. The
301
+ # minimal sub-forest preserving all required pair connections is therefore
302
+ # the union of the unique paths between each pair, so mark those paths
303
+ # directly instead of running O(|F|^2) reverse-delete passes that rebuild a
304
+ # UnionFind per candidate edge.
305
+ adjacency: Dict[int, List[Tuple[int, int]]] = collections.defaultdict(list)
306
+ for edge_idx, (node_u, node_v, _cost) in enumerate(F):
307
+ adjacency[node_u].append((node_v, edge_idx))
308
+ adjacency[node_v].append((node_u, edge_idx))
309
+
310
+ needed: List[bool] = [False] * len(F)
311
+ for source in sources:
312
+ parent_edge: Dict[int, int] = {}
313
+ parent_node: Dict[int, int] = {}
314
+ visited: Set[int] = {source}
315
+ stack: List[int] = [source]
316
+ while stack:
317
+ node = stack.pop()
318
+ for neighbor, edge_idx in adjacency[node]:
319
+ if neighbor not in visited:
320
+ visited.add(neighbor)
321
+ parent_edge[neighbor] = edge_idx
322
+ parent_node[neighbor] = node
323
+ stack.append(neighbor)
324
+ for target in pair_dict[source]:
325
+ current = target
326
+ while current != source:
327
+ edge_idx = parent_edge[current]
328
+ needed[edge_idx] = True
329
+ current = parent_node[current]
330
+
331
+ F_pruned: List[Tuple[int, int, float]] = [
332
+ edge for edge_idx, edge in enumerate(F) if needed[edge_idx]
333
+ ]
322
334
 
323
- # Compute cost
324
335
  total_cost: float = sum(cost for _, _, cost in F_pruned)
325
336
 
326
- # Identify Steiner nodes
327
337
  used_nodes: Set[int] = set()
328
338
  for node_u, node_v, _ in F_pruned:
329
339
  used_nodes.add(node_u)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: physdes-py
3
- Version: 0.8
3
+ Version: 0.9
4
4
  Summary: Physical Design Python Code
5
5
  Home-page: https://github.com/luk036/physdes-py/
6
6
  Author: Wai-Shing Luk
@@ -13,7 +13,6 @@ Classifier: Programming Language :: Python
13
13
  Description-Content-Type: text/markdown; charset=UTF-8
14
14
  Requires-Dist: typing_extensions
15
15
  Requires-Dist: mywheel
16
- Requires-Dist: icecream
17
16
  Provides-Extra: testing
18
17
  Requires-Dist: setuptools; extra == "testing"
19
18
  Requires-Dist: pytest; extra == "testing"
@@ -201,6 +201,7 @@ tests/mysvg.css
201
201
  tests/pnrpoly.svg
202
202
  tests/run_failing_tests.py
203
203
  tests/test_alpha.py
204
+ tests/test_benchmarks.py
204
205
  tests/test_clk_tree_vis.py
205
206
  tests/test_dme_algorithm.py
206
207
  tests/test_generic_functions.py