Skip to content

♻️ Layout: DenseMap storage and partial-injection API - #1956

Open
rturrado wants to merge 3 commits into
munich-quantum-toolkit:mainfrom
rturrado:1867
Open

♻️ Layout: DenseMap storage and partial-injection API#1956
rturrado wants to merge 3 commits into
munich-quantum-toolkit:mainfrom
rturrado:1867

Conversation

@rturrado

@rturrado rturrado commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

🤖 AI text below 🤖

Description

Two commits on Layout:

  1. Change Layout storage to DenseMap: replace Layout's SmallVector<size_t> storage with DenseMap<size_t, size_t> so indices no longer need to form a contiguous range [0, N).
  2. Prepare Layout API for partial injections: split nqubits() into nProgramQubits() / nHardwareQubits(), add hasProgramAt(hw), generalize swap to accept unplaced sides, and extend random to random(nProg, nHw, seed) so nProg < nHw leaves the remaining hardware slots unplaced.

The mapping pass keeps calling Layout::random(n, n, rng()) and nHardwareQubits(), so behavior is unchanged. Actually using nProg < nHw and the query API from inside the pass is left for a follow-up.

Part of #1867

Checklist

  • The pull request only contains commits that are focused and relevant to this change.
  • I have added appropriate tests that cover the new/changed functionality.
  • I have updated the documentation to reflect these changes.
  • I have added entries to the changelog for any noteworthy additions, changes, fixes, or removals.
  • I have added migration instructions to the upgrade guide (if needed).
  • The changes follow the project's style guidelines and introduce no new warnings.
  • The changes are fully tested and pass the CI checks.
  • I have reviewed my own code changes.

If PR contains AI-assisted content:

  • Any agent that created, edited, or submitted GitHub content was explicitly authorized for that scope, as required by our AI Usage Guidelines.
  • Every agent-authored or agent-edited public text body begins with the visible disclosure 🤖 *AI text below* 🤖 (titles are exempt).
  • AI-assisted commits include an Assisted-by: [Model Name] via [Tool Name] footer.
  • I confirm that I have personally reviewed and understood all AI-generated content, and accept full responsibility for it.

Assisted-by: Claude Opus 4.7 via Claude Code

@codecov

codecov Bot commented Jul 27, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 98.18182% with 1 line in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
...lir/lib/Dialect/QCO/Transforms/Mapping/Mapping.cpp 92.3% 1 Missing ⚠️

📢 Thoughts on this report? Let us know!

@burgholzer

Copy link
Copy Markdown
Member

@rturrado Thanks for another PR! 🙌

@MatthiasReumann care to take a first look here since this is closest to the stuff you have been working on? 😌

Comment on lines -1397 to -1434
const Layout exit =
TypeSwitch<Operation*, Layout>(op)
.Case<scf::ForOp>([&](scf::ForOp) {
// Find (insert) the epilogue SWAP sequence for (into) the child
// region using the restore strategy.

const auto swaps = restore(children[0].layout, parent.layout);
insertSWAPs<Mode>(swaps, children[0], stats, rewriter);
return parent.layout;
})
.template Case<scf::WhileOp>([&](scf::WhileOp) {
// Find (insert) the epilogue SWAP sequence for (into) the after
// region using the restore strategy.

const auto swaps = restore(children[1].layout, parent.layout);
insertSWAPs<Mode>(swaps, children[1], stats, rewriter);

// The scf::YieldOp is the terminator in the before region and
// thus determines the final output layout.
return children[0].layout;
})
.template Case<IfOp>([&](IfOp) {
// Find (insert) the epilogue SWAP sequence for (into) each child
// branch using the "converge" strategy.

const auto [convergedLayout, fst, snd] =
converge(children[0].layout, children[1].layout);
insertSWAPs<Mode>(fst, children[0], stats, rewriter);
insertSWAPs<Mode>(snd, children[1], stats, rewriter);
return convergedLayout;
})
.template Case<IndexSwitchOp>([&](IndexSwitchOp) {
for (auto& child : children) {
const auto swaps = restore(child.layout, parent.layout);
insertSWAPs<Mode>(swaps, child, stats, rewriter);
}
return parent.layout;
});

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.

The converge strategy for qco.if and the restore strategy only of the "yielding" branch of the scf.while operation is definitely not a bug but a feature.

@MatthiasReumann MatthiasReumann left a comment

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.

@rturrado Thanks for the effort 🚀 Really appreciate it!

I've left one comment regarding the strategies for mapping SCF which probably shouldn't be reverted to "restore-all". The essential idea is that the SCF operations (besides scf.for) act like permutation networks changing the layout (which needs to be propagated to the parent). If you have any questions, feel free to reach out!

@rturrado

Copy link
Copy Markdown
Contributor Author

@rturrado Thanks for another PR! 🙌

Thanks! This one touches a really beautiful area.

@MatthiasReumann care to take a first look here since this is closest to the stuff you have been working on? 😌

But I have to admit that it is also more complicated code, so human review here is essential.

@rturrado

Copy link
Copy Markdown
Contributor Author

@rturrado Thanks for the effort 🚀 Really appreciate it!

Many thanks!

I've left one comment regarding the strategies for mapping SCF which probably shouldn't be reverted to "restore-all". The essential idea is that the SCF operations (besides scf.for) act like permutation networks changing the layout (which needs to be propagated to the parent). If you have any questions, feel free to reach out!

Perfect, thanks! Yes, this was my main concern about this PR. That the new wire-index-equals-hardware-index invariant always has to restore to the parent layout and never converges. That which needs to be propagated to the parent constraint is key here.

@MatthiasReumann

Copy link
Copy Markdown
Collaborator

@rturrado

That the new wire-index-equals-hardware-index invariant always has to restore to the parent layout and never converges.

I think the "wire-index-equals-hardware-index" is a pretty neat idea, which eventually I would have also looked into. In a previous version of the mapping pass (before SCF mapping), we implemented "wire-index-equals-program-index" which worked pretty nicely.

Nonetheless, I think it would make sense to split this PR into two:

  • The layout improvements + everything else regarding getComputation, etc.
  • The "wire-index-equals-hardware-index" logic.

Especially since it's very likely that #1951 is merged before this one.

@rturrado

Copy link
Copy Markdown
Contributor Author

🤖 AI text below 🤖

@MatthiasReumann Agreed on the split. Rough sketch:

PR 1 (safe): Layout API additions (nProgramQubits, nHardwareQubits, hasProgramAt, swap with an empty side), the random(nProg, nHw, seed) variant that only places nProg real programs (still with placeholder fill), and any getComputation cleanup that does not depend on the invariant. Keeps WireInfos and the current dispatch (converge for qco.if, restore-yielding-only for scf.while, and vote-and-restore for qco.index_switch from #1951).

PR 2 (invariant): wire index == hardware index everywhere, sentinel WireIterator, delete WireInfos, random drops the placeholder fill, rewrite dispatch for the invariant.

Assisted-by: Claude Opus 4.7 via Claude Code

@rturrado rturrado changed the title ♻️ Layout: non-consecutive qubit indices and injective mapping ♻️ ♻️ Layout: DenseMap storage and partial-injection API Jul 28, 2026
@rturrado rturrado changed the title ♻️ ♻️ Layout: DenseMap storage and partial-injection API ♻️ Layout: DenseMap storage and partial-injection API Jul 28, 2026
@burgholzer

Copy link
Copy Markdown
Member

I think we'll try to get #1951 in asap (either tonight or tomorrow). Then PR1 can go on top 😌

@rturrado

Copy link
Copy Markdown
Contributor Author

I think we'll try to get #1951 in asap (either tonight or tomorrow). Then PR1 can go on top 😌

Perfect. Don't rush it. I'll rebase when you're done with #1951.

@mergify mergify Bot added the conflict label Aug 3, 2026
Replace the two `SmallVector<size_t>` fields inside `Layout` with two `DenseMap<size_t, size_t>` so that the storage no longer assumes program and hardware qubit indices form a contiguous range `[0, N)`.
This addresses the first point of munich-quantum-toolkit#1867 at the storage level; callers still add contiguous indices today.

`nqubits()` continues to return the size the layout was declared with, now stored explicitly in `nqubits_` since `DenseMap::size()` reports the number of placed entries rather than the declared capacity.
`add()` keeps its bounds check against `nqubits_` and gains an injectivity check via `!contains(prog)` / `!contains(hw)`.

Drop `getProgramToHardware()`.
It only made sense while the internal storage was already an array; under `DenseMap`, the materialization no longer belongs on `Layout`.
Its sole caller in `MappingPass::search` now owns the memoization keys locally in a `std::deque<SmallVector<size_t>>`; a materialization lambda builds each key from `layout.getHardwareIndex(prog)`, and `bestDepth` stays `DenseMap<ArrayRef<size_t>, size_t>`.
`std::deque` is used because `push_back` does not invalidate references to existing elements, so `ArrayRef` keys already stored in `bestDepth` stay stable as the deque grows.

Relaxing the mapping from a bijection to an injection (second point of munich-quantum-toolkit#1867) is addressed in a follow-up commit.

Part 1/2 of munich-quantum-toolkit#1867

Assisted-by: Claude Opus 4.7 via Claude Code
Signed-off-by: rturrado <rturrado@gmail.com>
@rturrado
rturrado force-pushed the 1867 branch 2 times, most recently from 3e25d7d to 25f536f Compare August 7, 2026 11:45
`Layout` can now describe partial program-to-hardware mappings.
`nqubits()` is split into `nProgramQubits()` and `nHardwareQubits()`.
`hasProgramAt(hw)` reports whether a hardware qubit currently carries a program qubit.
`swap(hwA, hwB)` accepts either side as unplaced and treats empty as a legitimate value that also gets swapped.
`random(nProg, nHw, seed)` places `nProg` program qubits on distinct hardware qubits drawn from `[0, nHw)`, so `nProg < nHw` leaves the remaining hardware slots unplaced.

The mapping pass keeps calling `random(n, n, rng())` and `nHardwareQubits()`, so behavior is unchanged.
Actually using `nProg < nHw` and the query API from inside the pass is left for a follow-up.

Part of munich-quantum-toolkit#1867

Assisted-by: Claude Opus 4.7 via Claude Code
Signed-off-by: rturrado <rturrado@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants