Skip to content

dft: implement scan_opt using the 2-Opt algorithm#10176

Open
donn wants to merge 1 commit intoThe-OpenROAD-Project:masterfrom
donn:scan_opt
Open

dft: implement scan_opt using the 2-Opt algorithm#10176
donn wants to merge 1 commit intoThe-OpenROAD-Project:masterfrom
donn:scan_opt

Conversation

@donn
Copy link
Copy Markdown
Contributor

@donn donn commented Apr 17, 2026

Summary

  • dft::Dft:
    • isolate writing the data to odb to its own function
    • store scan chains for later optimization after stitching
  • dft::Dft::scanOpt(): use scan chain driver/sink information plus instance placements to optimize the total scan chain length using the 2Opt algorithm
  • dft::ScanChain:
  • add estimateInternalTWL() to return an estimated total wire length for the scan chain based on cell origins (for verification/testing purposes)
  • sortScanCells now returns an estimated total wire length (and requires lambdas passed to it to do so as well)
  • dft::ScanPin: add getLocation() as a a method to get terminal locations whether it's a bterm or an iterm
  • odb::dbDft: Add reset() method to clear all existing info

Type of Change

  • New feature
  • Refactoring

Impact

Measurable reduction in routing time versus the unoptimized scan chains.

Verification

  • I have verified that the local build succeeds (./etc/Build.sh).
  • I have run the relevant tests and they pass.
  • My code follows the repository's formatting guidelines.
  • I have signed my commits (DCO).

- dft::Dft:
  - isolate writing the data to odb to its own function
  - store scan chains for later optimization after stitching
- dft::Dft::scanOpt(): use scan chain driver/sink information plus instance placements to optimize the total scan chain length using the 2Opt algorithm
- dft::ScanChain:
 - add estimateInternalTWL() to return an estimated total wire length for the scan chain based on cell origins (for verification/testing purposes)
 - sortScanCells now returns an estimated total wire length (and requires lambdas passed to it to do so as well)
- dft::ScanPin: add getLocation() as a a method to get terminal locations whether it's a bterm or an iterm
- odb::dbDft: Add reset() method to clear all existing info
- utl::Logger::error(): replace sprintf with snprintf so clang doesn't report an error on every single use of the function.

Signed-off-by: Mohamed Gaber <me@donn.website>
@donn
Copy link
Copy Markdown
Contributor Author

donn commented Apr 17, 2026

I know I still need to update the Readme but given the average turnaround on contributing to the dft module is about a month I figured may as well just get started and get feedback first

Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request implements scan chain optimization using a 2-Opt heuristic to reduce total wire length and introduces several architectural improvements to the DFT module, such as the writeToOdb method and state resetting in dbDft. Key feedback includes addressing a critical issue where a side effect inside an assert would be stripped in release builds, potentially causing incorrect behavior. Additionally, a return statement in the optimization loop should be changed to continue to avoid skipping valid chains when one fails, and an initialization of a size_t index to -1 needs to be corrected to prevent out-of-bounds access.

}
points[++i] = sink;
score += odb::Point::manhattanDistance(last, sink);
assert(++i == N);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

critical

The assert contains a side effect (++i). In release builds where NDEBUG is defined, the assertion and its expression are removed, meaning i will not be incremented. This will lead to incorrect behavior in release builds. Side effects should be moved outside of the assert.

Suggested change
assert(++i == N);
++i;
assert(i == N);

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well i is not used after this point but fair enough

Comment thread src/dft/src/Dft.cpp
14,
"Cannot optimize: source/sink pins don't exist or have no "
"placement.");
return;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The return statement here will cause the entire scanOpt function to exit if a single scan chain cannot be optimized (e.g., due to missing source/sink pins or placement). This prevents other valid scan chains from being optimized. It should be replaced with continue to skip only the current chain.

Suggested change
return;
continue;

size_t N = cells.size() + 2;

std::vector<odb::Point> points(N);
size_t i = -1;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

In this repository, static_cast<size_t>(-1) is the standard sentinel value for an invalid size_t index because 0 is often a valid index. However, using -1 (which wraps to the maximum value of size_t) as an active index for points[i] will cause an out-of-bounds access. If i is intended to be a valid starting index, it should be initialized to 0.

Suggested change
size_t i = -1;
size_t i = 0;
points[i] = source;
References
  1. When checking for failure or sentinel values in size_t, use static_cast<size_t>(-1) because 0 is a valid index.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Doing this instead seems a bit clearer and achieves the same thing.

size_t i = 0;
points[i++] = source;

Copy link
Copy Markdown
Contributor

@github-actions github-actions bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

clang-tidy made some suggestions

There were too many comments to post at once. Showing the first 25 out of 28. Check the log or trigger a new build to see more.

Comment thread src/dft/src/Dft.cpp
return;
}

odb::Point src = odb::Point(src_x, src_y);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

warning: no header providing "odb::Point" is directly included [misc-include-cleaner]

src/dft/src/Dft.cpp:25:

- #include "utl/Logger.h"
+ #include "odb/geom.h"
+ #include "utl/Logger.h"

Comment thread src/dft/src/Dft.cpp
odb::Point src = odb::Point(src_x, src_y);
odb::Point sink = odb::Point(sink_x, sink_y);

int64_t twl_internal = chain->estimateInternalTWL();
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

warning: no header providing "int64_t" is directly included [misc-include-cleaner]

src/dft/src/Dft.cpp:5:

- #include <memory>
+ #include <cstdint>
+ #include <memory>

Comment thread src/dft/src/Dft.cpp
int64_t twl_internal = chain->estimateInternalTWL();
int64_t twl = twl_internal;
const auto& scan_cells = chain->getScanCells();
if (scan_cells.size() > 0) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

warning: the 'empty' method should be used to check for emptiness instead of 'size' [readability-container-size-empty]

Suggested change
if (scan_cells.size() > 0) {
if (!scan_cells.empty()) {
Additional context

/usr/include/c++/13/bits/stl_vector.h:1087: method 'vector'::empty() defined here

      empty() const _GLIBCXX_NOEXCEPT
      ^

Comment thread src/dft/src/Dft.cpp
// Sort to reduce wire length
odb::Point f_src(src_x, src_y);
odb::Point f_sink(sink_x, sink_y);
if (rising.size() > 0) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

warning: the 'empty' method should be used to check for emptiness instead of 'size' [readability-container-size-empty]

Suggested change
if (rising.size() > 0) {
if (!rising.empty()) {
Additional context

/usr/include/c++/13/bits/stl_vector.h:1087: method 'vector'::empty() defined here

      empty() const _GLIBCXX_NOEXCEPT
      ^

Comment thread src/dft/src/Dft.cpp
= OptimizeScanWirelength2Opt(f_src, f_sink, falling, logger_);

odb::Point r_src(src_x, src_y);
if (falling.size() > 0) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

warning: the 'empty' method should be used to check for emptiness instead of 'size' [readability-container-size-empty]

Suggested change
if (falling.size() > 0) {
if (!falling.empty()) {
Additional context

/usr/include/c++/13/bits/stl_vector.h:1087: method 'vector'::empty() defined here

      empty() const _GLIBCXX_NOEXCEPT
      ^

utl::Logger* logger);

int64_t OptimizeScanWirelength2Opt(
const odb::Point source,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

warning: parameter 'source' is const-qualified in the function declaration; const-qualification of parameters only has an effect in function definitions [readability-avoid-const-params-in-decls]

Suggested change
const odb::Point source,
odb::Point source,


int64_t OptimizeScanWirelength2Opt(
const odb::Point source,
const odb::Point sink,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

warning: parameter 'sink' is const-qualified in the function declaration; const-qualification of parameters only has an effect in function definitions [readability-avoid-const-params-in-decls]

Suggested change
const odb::Point sink,
odb::Point sink,

const odb::Point sink,
std::vector<std::unique_ptr<ScanCell>>& cells,
utl::Logger* logger,
size_t max_iters = 30);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

warning: no header providing "size_t" is directly included [misc-include-cleaner]

src/dft/src/architect/Opt.hh:5:

- #include <memory>
+ #include <cstddef>
+ #include <memory>

= OptimizeScanWirelengthNN(falling, logger_);
auto rising_wire_length = OptimizeScanWirelengthNN(rising, logger_);
int64_t distance_between_falling_and_rising = 0;
if (rising.size() && falling.size()) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

warning: the 'empty' method should be used to check for emptiness instead of 'size' [readability-container-size-empty]

Suggested change
if (rising.size() && falling.size()) {
if (!rising.empty() && falling.size()) {
Additional context

/usr/include/c++/13/bits/stl_vector.h:1087: method 'vector'::empty() defined here

      empty() const _GLIBCXX_NOEXCEPT
      ^

= OptimizeScanWirelengthNN(falling, logger_);
auto rising_wire_length = OptimizeScanWirelengthNN(rising, logger_);
int64_t distance_between_falling_and_rising = 0;
if (rising.size() && falling.size()) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

warning: the 'empty' method should be used to check for emptiness instead of 'size' [readability-container-size-empty]

Suggested change
if (rising.size() && falling.size()) {
if (rising.size() && !falling.empty()) {
Additional context

/usr/include/c++/13/bits/stl_vector.h:1087: method 'vector'::empty() defined here

      empty() const _GLIBCXX_NOEXCEPT
      ^

@donn
Copy link
Copy Markdown
Contributor Author

donn commented Apr 17, 2026

Anyone have advice on how I should make dbDft.reset "generated"

@maliberty
Copy link
Copy Markdown
Member

@fgaray review if you have time.

Copy link
Copy Markdown
Member

@maliberty maliberty left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Needs a unit test

Comment thread .gitignore
build_linux
build_mac
test/results
/sandbox
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why is this needed?

return total_wire_length;
}

int64_t ScanChain::estimateInternalTWL()
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TWL = total wire length?? A more descriptive name would be helpful

return 0;
}

size_t N = cells.size() + 2;
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

use snake_case for variables, a more meaningful name, and make it const


int64_t best_score = score;

// Already "optimal" (unplaced)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can't be unplaced as we already returned on unplaced above.

const odb::Point sink,
std::vector<std::unique_ptr<ScanCell>>& cells,
utl::Logger* logger,
size_t max_iters)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

const

@donn
Copy link
Copy Markdown
Contributor Author

donn commented Apr 18, 2026

Will implement all suggestions when I get a moment 🫡

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants