diff --git a/CHANGELOG.md b/CHANGELOG.md index aebbffbea1..7bc948d83f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -58,10 +58,10 @@ releases may include breaking changes. - ✨ Add passes for quantum-specific interprocedural optimizations ([#2193]) ([**@DRovara**], [**@burgholzer**]) -- ✨ Add Pauli twirling, quantum loop unrolling, and qubit reuse passes - ([#1705], [#1718], [#1755], [#1756], [#1923], [#1924], [#2039], [#2118], - [#2216], [#2224]) ([**@MatthiasReumann**], [**@DRovara**], [**@burgholzer**], - [**@simon1hofmann**]) +- ✨ Add Pauli twirling, quantum loop unrolling, qubit reuse passes, and + constant propagation ([#1705], [#1718], [#1755], [#1756], [#1923], [#1924], + [#2039], [#2118], [#2216], [#2224], [#2280]) ([**@MatthiasReumann**], + [**@DRovara**], [**@burgholzer**], [**@simon1hofmann**], [**@lirem101**]) - ✨ Add a compiler-target-aware `place-and-route` pass ([#1537], [#1547], [#1568], [#1581], [#1583], [#1588], [#1600], [#1664], [#1709], [#1716], [#1748], [#1805], [#1870], [#1904], [#1911], [#1951], [#1997], [#2016], @@ -834,469 +834,925 @@ for previous changelogs._ [unreleased]: https://github.com/munich-quantum-toolkit/core/compare/v3.9.2...HEAD + [3.9.2]: https://github.com/munich-quantum-toolkit/core/releases/tag/v3.9.2 + [3.9.1]: https://github.com/munich-quantum-toolkit/core/releases/tag/v3.9.1 + [3.9.0]: https://github.com/munich-quantum-toolkit/core/releases/tag/v3.9.0 + [3.8.0]: https://github.com/munich-quantum-toolkit/core/releases/tag/v3.8.0 + [3.7.0]: https://github.com/munich-quantum-toolkit/core/releases/tag/v3.7.0 + [3.6.1]: https://github.com/munich-quantum-toolkit/core/releases/tag/v3.6.1 + [3.6.0]: https://github.com/munich-quantum-toolkit/core/releases/tag/v3.6.0 + [3.5.1]: https://github.com/munich-quantum-toolkit/core/releases/tag/v3.5.1 + [3.5.0]: https://github.com/munich-quantum-toolkit/core/releases/tag/v3.5.0 + [3.4.1]: https://github.com/munich-quantum-toolkit/core/releases/tag/v3.4.1 + [3.4.0]: https://github.com/munich-quantum-toolkit/core/releases/tag/v3.4.0 + [3.3.3]: https://github.com/munich-quantum-toolkit/core/releases/tag/v3.3.3 + [3.3.2]: https://github.com/munich-quantum-toolkit/core/releases/tag/v3.3.2 + [3.3.1]: https://github.com/munich-quantum-toolkit/core/releases/tag/v3.3.1 + [3.3.0]: https://github.com/munich-quantum-toolkit/core/releases/tag/v3.3.0 + [3.2.1]: https://github.com/munich-quantum-toolkit/core/releases/tag/v3.2.1 + [3.2.0]: https://github.com/munich-quantum-toolkit/core/releases/tag/v3.2.0 + [3.1.0]: https://github.com/munich-quantum-toolkit/core/releases/tag/v3.1.0 + [3.0.2]: https://github.com/munich-quantum-toolkit/core/releases/tag/v3.0.2 + [3.0.1]: https://github.com/munich-quantum-toolkit/core/releases/tag/v3.0.1 + [3.0.0]: https://github.com/munich-quantum-toolkit/core/releases/tag/v3.0.0 + [2.7.0]: https://github.com/munich-quantum-toolkit/core/releases/tag/v2.7.0 +[#2280]: https://github.com/munich-quantum-toolkit/core/pull/2280 + [#2278]: https://github.com/munich-quantum-toolkit/core/pull/2278 + [#2270]: https://github.com/munich-quantum-toolkit/core/pull/2270 + [#2262]: https://github.com/munich-quantum-toolkit/core/pull/2262 + [#2259]: https://github.com/munich-quantum-toolkit/core/pull/2259 + [#2258]: https://github.com/munich-quantum-toolkit/core/pull/2258 + [#2257]: https://github.com/munich-quantum-toolkit/core/pull/2257 + [#2249]: https://github.com/munich-quantum-toolkit/core/pull/2249 + [#2246]: https://github.com/munich-quantum-toolkit/core/pull/2246 + [#2220]: https://github.com/munich-quantum-toolkit/core/pull/2220 + [#2232]: https://github.com/munich-quantum-toolkit/core/pull/2232 + [#2228]: https://github.com/munich-quantum-toolkit/core/pull/2228 + [#2224]: https://github.com/munich-quantum-toolkit/core/pull/2224 + [#2209]: https://github.com/munich-quantum-toolkit/core/pull/2209 + [#2211]: https://github.com/munich-quantum-toolkit/core/pull/2211 + [#2217]: https://github.com/munich-quantum-toolkit/core/pull/2217 + [#2210]: https://github.com/munich-quantum-toolkit/core/pull/2210 + [#2216]: https://github.com/munich-quantum-toolkit/core/pull/2216 + [#2203]: https://github.com/munich-quantum-toolkit/core/pull/2203 + [#2214]: https://github.com/munich-quantum-toolkit/core/pull/2214 + [#2193]: https://github.com/munich-quantum-toolkit/core/pull/2193 + [#2178]: https://github.com/munich-quantum-toolkit/core/pull/2178 + [#2176]: https://github.com/munich-quantum-toolkit/core/pull/2176 + [#2175]: https://github.com/munich-quantum-toolkit/core/pull/2175 + [#2169]: https://github.com/munich-quantum-toolkit/core/pull/2169 + [#2168]: https://github.com/munich-quantum-toolkit/core/pull/2168 + [#2158]: https://github.com/munich-quantum-toolkit/core/pull/2158 + [#2157]: https://github.com/munich-quantum-toolkit/core/pull/2157 + [#2156]: https://github.com/munich-quantum-toolkit/core/pull/2156 + [#2154]: https://github.com/munich-quantum-toolkit/core/pull/2154 + [#2150]: https://github.com/munich-quantum-toolkit/core/pull/2150 + [#2149]: https://github.com/munich-quantum-toolkit/core/pull/2149 + [#2148]: https://github.com/munich-quantum-toolkit/core/pull/2148 + [#2147]: https://github.com/munich-quantum-toolkit/core/pull/2147 + [#2141]: https://github.com/munich-quantum-toolkit/core/pull/2141 + [#2140]: https://github.com/munich-quantum-toolkit/core/pull/2140 + [#2138]: https://github.com/munich-quantum-toolkit/core/pull/2138 + [#2137]: https://github.com/munich-quantum-toolkit/core/pull/2137 + [#2136]: https://github.com/munich-quantum-toolkit/core/pull/2136 + [#2133]: https://github.com/munich-quantum-toolkit/core/pull/2133 + [#2125]: https://github.com/munich-quantum-toolkit/core/pull/2125 + [#2124]: https://github.com/munich-quantum-toolkit/core/pull/2124 + [#2118]: https://github.com/munich-quantum-toolkit/core/pull/2118 + [#2116]: https://github.com/munich-quantum-toolkit/core/pull/2116 + [#2115]: https://github.com/munich-quantum-toolkit/core/pull/2115 + [#2114]: https://github.com/munich-quantum-toolkit/core/pull/2114 + [#2112]: https://github.com/munich-quantum-toolkit/core/pull/2112 + [#2111]: https://github.com/munich-quantum-toolkit/core/pull/2111 + [#2108]: https://github.com/munich-quantum-toolkit/core/pull/2108 + [#2106]: https://github.com/munich-quantum-toolkit/core/pull/2106 + [#2105]: https://github.com/munich-quantum-toolkit/core/pull/2105 + [#2084]: https://github.com/munich-quantum-toolkit/core/pull/2084 + [#2082]: https://github.com/munich-quantum-toolkit/core/pull/2082 + [#2074]: https://github.com/munich-quantum-toolkit/core/pull/2074 + [#2066]: https://github.com/munich-quantum-toolkit/core/pull/2066 + [#2060]: https://github.com/munich-quantum-toolkit/core/pull/2060 + [#2058]: https://github.com/munich-quantum-toolkit/core/pull/2058 + [#2054]: https://github.com/munich-quantum-toolkit/core/pull/2054 + [#2049]: https://github.com/munich-quantum-toolkit/core/pull/2049 + [#2046]: https://github.com/munich-quantum-toolkit/core/pull/2046 + [#2043]: https://github.com/munich-quantum-toolkit/core/pull/2043 + [#2042]: https://github.com/munich-quantum-toolkit/core/pull/2042 + [#2039]: https://github.com/munich-quantum-toolkit/core/pull/2039 + [#2038]: https://github.com/munich-quantum-toolkit/core/pull/2038 + [#2036]: https://github.com/munich-quantum-toolkit/core/pull/2036 + [#2035]: https://github.com/munich-quantum-toolkit/core/pull/2035 + [#2031]: https://github.com/munich-quantum-toolkit/core/pull/2031 + [#2030]: https://github.com/munich-quantum-toolkit/core/pull/2030 + [#2028]: https://github.com/munich-quantum-toolkit/core/pull/2028 + [#2026]: https://github.com/munich-quantum-toolkit/core/pull/2026 + [#2025]: https://github.com/munich-quantum-toolkit/core/pull/2025 + [#2018]: https://github.com/munich-quantum-toolkit/core/pull/2018 + [#2017]: https://github.com/munich-quantum-toolkit/core/pull/2017 + [#2016]: https://github.com/munich-quantum-toolkit/core/pull/2016 + [#2015]: https://github.com/munich-quantum-toolkit/core/pull/2015 + [#2014]: https://github.com/munich-quantum-toolkit/core/pull/2014 + [#2011]: https://github.com/munich-quantum-toolkit/core/pull/2011 + [#2010]: https://github.com/munich-quantum-toolkit/core/pull/2010 + [#2008]: https://github.com/munich-quantum-toolkit/core/pull/2008 + [#2007]: https://github.com/munich-quantum-toolkit/core/pull/2007 + [#2006]: https://github.com/munich-quantum-toolkit/core/pull/2006 + [#2005]: https://github.com/munich-quantum-toolkit/core/pull/2005 + [#2003]: https://github.com/munich-quantum-toolkit/core/pull/2003 + [#2002]: https://github.com/munich-quantum-toolkit/core/pull/2002 + [#2001]: https://github.com/munich-quantum-toolkit/core/pull/2001 + [#2000]: https://github.com/munich-quantum-toolkit/core/pull/2000 + [#1999]: https://github.com/munich-quantum-toolkit/core/pull/1999 + [#1998]: https://github.com/munich-quantum-toolkit/core/pull/1998 + [#1997]: https://github.com/munich-quantum-toolkit/core/pull/1997 + [#1996]: https://github.com/munich-quantum-toolkit/core/pull/1996 + [#1995]: https://github.com/munich-quantum-toolkit/core/pull/1995 + [#1994]: https://github.com/munich-quantum-toolkit/core/pull/1994 + [#1993]: https://github.com/munich-quantum-toolkit/core/pull/1993 + [#1992]: https://github.com/munich-quantum-toolkit/core/pull/1992 + [#1989]: https://github.com/munich-quantum-toolkit/core/pull/1989 + [#1987]: https://github.com/munich-quantum-toolkit/core/pull/1987 + [#1986]: https://github.com/munich-quantum-toolkit/core/pull/1986 + [#1984]: https://github.com/munich-quantum-toolkit/core/pull/1984 + [#1983]: https://github.com/munich-quantum-toolkit/core/pull/1983 + [#1980]: https://github.com/munich-quantum-toolkit/core/pull/1980 + [#1979]: https://github.com/munich-quantum-toolkit/core/pull/1979 + [#1978]: https://github.com/munich-quantum-toolkit/core/pull/1978 + [#1976]: https://github.com/munich-quantum-toolkit/core/pull/1976 + [#1975]: https://github.com/munich-quantum-toolkit/core/pull/1975 + [#1974]: https://github.com/munich-quantum-toolkit/core/pull/1974 + [#1973]: https://github.com/munich-quantum-toolkit/core/pull/1973 + [#1972]: https://github.com/munich-quantum-toolkit/core/pull/1972 + [#1967]: https://github.com/munich-quantum-toolkit/core/pull/1967 + [#1965]: https://github.com/munich-quantum-toolkit/core/pull/1965 + [#1961]: https://github.com/munich-quantum-toolkit/core/pull/1961 + [#1957]: https://github.com/munich-quantum-toolkit/core/pull/1957 + [#1953]: https://github.com/munich-quantum-toolkit/core/pull/1953 + [#1952]: https://github.com/munich-quantum-toolkit/core/pull/1952 + [#1951]: https://github.com/munich-quantum-toolkit/core/pull/1951 + [#1950]: https://github.com/munich-quantum-toolkit/core/pull/1950 + [#1938]: https://github.com/munich-quantum-toolkit/core/pull/1938 + [#1936]: https://github.com/munich-quantum-toolkit/core/pull/1936 + [#1935]: https://github.com/munich-quantum-toolkit/core/pull/1935 + [#1934]: https://github.com/munich-quantum-toolkit/core/pull/1934 + [#1933]: https://github.com/munich-quantum-toolkit/core/pull/1933 + [#1927]: https://github.com/munich-quantum-toolkit/core/pull/1927 + [#1925]: https://github.com/munich-quantum-toolkit/core/pull/1925 + [#1924]: https://github.com/munich-quantum-toolkit/core/pull/1924 + [#1923]: https://github.com/munich-quantum-toolkit/core/pull/1923 + [#1915]: https://github.com/munich-quantum-toolkit/core/pull/1915 + [#1914]: https://github.com/munich-quantum-toolkit/core/pull/1914 + [#1912]: https://github.com/munich-quantum-toolkit/core/pull/1912 + [#1911]: https://github.com/munich-quantum-toolkit/core/pull/1911 + [#1910]: https://github.com/munich-quantum-toolkit/core/pull/1910 + [#1904]: https://github.com/munich-quantum-toolkit/core/pull/1904 + [#1897]: https://github.com/munich-quantum-toolkit/core/pull/1897 + [#1895]: https://github.com/munich-quantum-toolkit/core/pull/1895 + [#1887]: https://github.com/munich-quantum-toolkit/core/pull/1887 + [#1886]: https://github.com/munich-quantum-toolkit/core/pull/1886 + [#1877]: https://github.com/munich-quantum-toolkit/core/pull/1877 + [#1873]: https://github.com/munich-quantum-toolkit/core/pull/1873 + [#1872]: https://github.com/munich-quantum-toolkit/core/pull/1872 + [#1870]: https://github.com/munich-quantum-toolkit/core/pull/1870 + [#1869]: https://github.com/munich-quantum-toolkit/core/pull/1869 + [#1865]: https://github.com/munich-quantum-toolkit/core/pull/1865 + [#1850]: https://github.com/munich-quantum-toolkit/core/pull/1850 + [#1849]: https://github.com/munich-quantum-toolkit/core/pull/1849 + [#1848]: https://github.com/munich-quantum-toolkit/core/pull/1848 + [#1844]: https://github.com/munich-quantum-toolkit/core/pull/1844 + [#1842]: https://github.com/munich-quantum-toolkit/core/pull/1842 + [#1836]: https://github.com/munich-quantum-toolkit/core/pull/1836 + [#1832]: https://github.com/munich-quantum-toolkit/core/pull/1832 + [#1830]: https://github.com/munich-quantum-toolkit/core/pull/1830 + [#1828]: https://github.com/munich-quantum-toolkit/core/pull/1828 + [#1826]: https://github.com/munich-quantum-toolkit/core/pull/1826 + [#1824]: https://github.com/munich-quantum-toolkit/core/pull/1824 + [#1823]: https://github.com/munich-quantum-toolkit/core/pull/1823 + [#1817]: https://github.com/munich-quantum-toolkit/core/pull/1817 + [#1815]: https://github.com/munich-quantum-toolkit/core/pull/1815 + [#1814]: https://github.com/munich-quantum-toolkit/core/pull/1814 + [#1810]: https://github.com/munich-quantum-toolkit/core/pull/1810 + [#1809]: https://github.com/munich-quantum-toolkit/core/pull/1809 + [#1808]: https://github.com/munich-quantum-toolkit/core/pull/1808 + [#1807]: https://github.com/munich-quantum-toolkit/core/pull/1807 + [#1806]: https://github.com/munich-quantum-toolkit/core/pull/1806 + [#1805]: https://github.com/munich-quantum-toolkit/core/pull/1805 + [#1803]: https://github.com/munich-quantum-toolkit/core/pull/1803 + [#1802]: https://github.com/munich-quantum-toolkit/core/pull/1802 + [#1799]: https://github.com/munich-quantum-toolkit/core/pull/1799 + [#1787]: https://github.com/munich-quantum-toolkit/core/pull/1787 + [#1786]: https://github.com/munich-quantum-toolkit/core/pull/1786 + [#1782]: https://github.com/munich-quantum-toolkit/core/pull/1782 + [#1781]: https://github.com/munich-quantum-toolkit/core/pull/1781 + [#1780]: https://github.com/munich-quantum-toolkit/core/pull/1780 + [#1776]: https://github.com/munich-quantum-toolkit/core/pull/1776 + [#1774]: https://github.com/munich-quantum-toolkit/core/pull/1774 + [#1766]: https://github.com/munich-quantum-toolkit/core/pull/1766 + [#1765]: https://github.com/munich-quantum-toolkit/core/pull/1765 + [#1762]: https://github.com/munich-quantum-toolkit/core/pull/1762 + [#1756]: https://github.com/munich-quantum-toolkit/core/pull/1756 + [#1755]: https://github.com/munich-quantum-toolkit/core/pull/1755 + [#1751]: https://github.com/munich-quantum-toolkit/core/pull/1751 + [#1749]: https://github.com/munich-quantum-toolkit/core/pull/1749 + [#1748]: https://github.com/munich-quantum-toolkit/core/pull/1748 + [#1737]: https://github.com/munich-quantum-toolkit/core/pull/1737 + [#1730]: https://github.com/munich-quantum-toolkit/core/pull/1730 + [#1728]: https://github.com/munich-quantum-toolkit/core/pull/1728 + [#1720]: https://github.com/munich-quantum-toolkit/core/pull/1720 + [#1719]: https://github.com/munich-quantum-toolkit/core/pull/1719 + [#1718]: https://github.com/munich-quantum-toolkit/core/pull/1718 + [#1717]: https://github.com/munich-quantum-toolkit/core/pull/1717 + [#1716]: https://github.com/munich-quantum-toolkit/core/pull/1716 + [#1710]: https://github.com/munich-quantum-toolkit/core/pull/1710 + [#1709]: https://github.com/munich-quantum-toolkit/core/pull/1709 + [#1706]: https://github.com/munich-quantum-toolkit/core/pull/1706 + [#1705]: https://github.com/munich-quantum-toolkit/core/pull/1705 + [#1702]: https://github.com/munich-quantum-toolkit/core/pull/1702 + [#1700]: https://github.com/munich-quantum-toolkit/core/pull/1700 + [#1694]: https://github.com/munich-quantum-toolkit/core/pull/1694 + [#1687]: https://github.com/munich-quantum-toolkit/core/pull/1687 + [#1676]: https://github.com/munich-quantum-toolkit/core/pull/1676 + [#1675]: https://github.com/munich-quantum-toolkit/core/pull/1675 + [#1674]: https://github.com/munich-quantum-toolkit/core/pull/1674 + [#1673]: https://github.com/munich-quantum-toolkit/core/pull/1673 + [#1672]: https://github.com/munich-quantum-toolkit/core/pull/1672 + [#1664]: https://github.com/munich-quantum-toolkit/core/pull/1664 + [#1662]: https://github.com/munich-quantum-toolkit/core/pull/1662 + [#1660]: https://github.com/munich-quantum-toolkit/core/pull/1660 + [#1652]: https://github.com/munich-quantum-toolkit/core/pull/1652 + [#1648]: https://github.com/munich-quantum-toolkit/core/pull/1648 + [#1638]: https://github.com/munich-quantum-toolkit/core/pull/1638 + [#1637]: https://github.com/munich-quantum-toolkit/core/pull/1637 + [#1635]: https://github.com/munich-quantum-toolkit/core/pull/1635 + [#1627]: https://github.com/munich-quantum-toolkit/core/pull/1627 + [#1626]: https://github.com/munich-quantum-toolkit/core/pull/1626 + [#1624]: https://github.com/munich-quantum-toolkit/core/pull/1624 + [#1623]: https://github.com/munich-quantum-toolkit/core/pull/1623 + [#1620]: https://github.com/munich-quantum-toolkit/core/pull/1620 + [#1605]: https://github.com/munich-quantum-toolkit/core/pull/1605 + [#1603]: https://github.com/munich-quantum-toolkit/core/pull/1603 + [#1602]: https://github.com/munich-quantum-toolkit/core/pull/1602 + [#1600]: https://github.com/munich-quantum-toolkit/core/pull/1600 + [#1596]: https://github.com/munich-quantum-toolkit/core/pull/1596 + [#1593]: https://github.com/munich-quantum-toolkit/core/pull/1593 + [#1588]: https://github.com/munich-quantum-toolkit/core/pull/1588 + [#1583]: https://github.com/munich-quantum-toolkit/core/pull/1583 + [#1581]: https://github.com/munich-quantum-toolkit/core/pull/1581 + [#1580]: https://github.com/munich-quantum-toolkit/core/pull/1580 + [#1573]: https://github.com/munich-quantum-toolkit/core/pull/1573 + [#1572]: https://github.com/munich-quantum-toolkit/core/pull/1572 + [#1571]: https://github.com/munich-quantum-toolkit/core/pull/1571 + [#1570]: https://github.com/munich-quantum-toolkit/core/pull/1570 + [#1569]: https://github.com/munich-quantum-toolkit/core/pull/1569 + [#1568]: https://github.com/munich-quantum-toolkit/core/pull/1568 + [#1567]: https://github.com/munich-quantum-toolkit/core/pull/1567 + [#1565]: https://github.com/munich-quantum-toolkit/core/pull/1565 + [#1564]: https://github.com/munich-quantum-toolkit/core/pull/1564 + [#1554]: https://github.com/munich-quantum-toolkit/core/pull/1554 + [#1550]: https://github.com/munich-quantum-toolkit/core/pull/1550 + [#1549]: https://github.com/munich-quantum-toolkit/core/pull/1549 + [#1548]: https://github.com/munich-quantum-toolkit/core/pull/1548 + [#1547]: https://github.com/munich-quantum-toolkit/core/pull/1547 + [#1542]: https://github.com/munich-quantum-toolkit/core/pull/1542 + [#1537]: https://github.com/munich-quantum-toolkit/core/pull/1537 + [#1528]: https://github.com/munich-quantum-toolkit/core/pull/1528 + [#1521]: https://github.com/munich-quantum-toolkit/core/pull/1521 + [#1513]: https://github.com/munich-quantum-toolkit/core/pull/1513 + [#1510]: https://github.com/munich-quantum-toolkit/core/pull/1510 + [#1507]: https://github.com/munich-quantum-toolkit/core/pull/1507 + [#1506]: https://github.com/munich-quantum-toolkit/core/pull/1506 + [#1481]: https://github.com/munich-quantum-toolkit/core/pull/1481 + [#1479]: https://github.com/munich-quantum-toolkit/core/pull/1479 + [#1475]: https://github.com/munich-quantum-toolkit/core/pull/1475 + [#1474]: https://github.com/munich-quantum-toolkit/core/pull/1474 + [#1472]: https://github.com/munich-quantum-toolkit/core/pull/1472 + [#1471]: https://github.com/munich-quantum-toolkit/core/pull/1471 + [#1470]: https://github.com/munich-quantum-toolkit/core/pull/1470 + [#1466]: https://github.com/munich-quantum-toolkit/core/pull/1466 + [#1465]: https://github.com/munich-quantum-toolkit/core/pull/1465 + [#1464]: https://github.com/munich-quantum-toolkit/core/pull/1464 + [#1458]: https://github.com/munich-quantum-toolkit/core/pull/1458 + [#1453]: https://github.com/munich-quantum-toolkit/core/pull/1453 + [#1447]: https://github.com/munich-quantum-toolkit/core/pull/1447 + [#1446]: https://github.com/munich-quantum-toolkit/core/pull/1446 + [#1444]: https://github.com/munich-quantum-toolkit/core/pull/1444 + [#1443]: https://github.com/munich-quantum-toolkit/core/pull/1443 + [#1437]: https://github.com/munich-quantum-toolkit/core/pull/1437 + [#1436]: https://github.com/munich-quantum-toolkit/core/pull/1436 + [#1430]: https://github.com/munich-quantum-toolkit/core/pull/1430 + [#1428]: https://github.com/munich-quantum-toolkit/core/pull/1428 + [#1415]: https://github.com/munich-quantum-toolkit/core/pull/1415 + [#1414]: https://github.com/munich-quantum-toolkit/core/pull/1414 + [#1413]: https://github.com/munich-quantum-toolkit/core/pull/1413 + [#1412]: https://github.com/munich-quantum-toolkit/core/pull/1412 + [#1411]: https://github.com/munich-quantum-toolkit/core/pull/1411 + [#1407]: https://github.com/munich-quantum-toolkit/core/pull/1407 + [#1406]: https://github.com/munich-quantum-toolkit/core/pull/1406 + [#1403]: https://github.com/munich-quantum-toolkit/core/pull/1403 + [#1402]: https://github.com/munich-quantum-toolkit/core/pull/1402 + [#1385]: https://github.com/munich-quantum-toolkit/core/pull/1385 + [#1384]: https://github.com/munich-quantum-toolkit/core/pull/1384 + [#1383]: https://github.com/munich-quantum-toolkit/core/pull/1383 + [#1382]: https://github.com/munich-quantum-toolkit/core/pull/1382 + [#1381]: https://github.com/munich-quantum-toolkit/core/pull/1381 + [#1380]: https://github.com/munich-quantum-toolkit/core/pull/1380 + [#1378]: https://github.com/munich-quantum-toolkit/core/pull/1378 + [#1375]: https://github.com/munich-quantum-toolkit/core/pull/1375 + [#1371]: https://github.com/munich-quantum-toolkit/core/pull/1371 + [#1359]: https://github.com/munich-quantum-toolkit/core/pull/1359 + [#1356]: https://github.com/munich-quantum-toolkit/core/pull/1356 + [#1355]: https://github.com/munich-quantum-toolkit/core/pull/1355 + [#1338]: https://github.com/munich-quantum-toolkit/core/pull/1338 + [#1336]: https://github.com/munich-quantum-toolkit/core/pull/1336 + [#1330]: https://github.com/munich-quantum-toolkit/core/pull/1330 + [#1328]: https://github.com/munich-quantum-toolkit/core/pull/1328 + [#1327]: https://github.com/munich-quantum-toolkit/core/pull/1327 + [#1310]: https://github.com/munich-quantum-toolkit/core/pull/1310 + [#1301]: https://github.com/munich-quantum-toolkit/core/pull/1301 + [#1300]: https://github.com/munich-quantum-toolkit/core/pull/1300 + [#1299]: https://github.com/munich-quantum-toolkit/core/pull/1299 + [#1294]: https://github.com/munich-quantum-toolkit/core/pull/1294 + [#1293]: https://github.com/munich-quantum-toolkit/core/pull/1293 + [#1287]: https://github.com/munich-quantum-toolkit/core/pull/1287 + [#1283]: https://github.com/munich-quantum-toolkit/core/pull/1283 + [#1279]: https://github.com/munich-quantum-toolkit/core/pull/1279 + [#1276]: https://github.com/munich-quantum-toolkit/core/pull/1276 + [#1271]: https://github.com/munich-quantum-toolkit/core/pull/1271 + [#1269]: https://github.com/munich-quantum-toolkit/core/pull/1269 + [#1264]: https://github.com/munich-quantum-toolkit/core/pull/1264 + [#1263]: https://github.com/munich-quantum-toolkit/core/pull/1263 + [#1247]: https://github.com/munich-quantum-toolkit/core/pull/1247 + [#1246]: https://github.com/munich-quantum-toolkit/core/pull/1246 + [#1243]: https://github.com/munich-quantum-toolkit/core/pull/1243 + [#1237]: https://github.com/munich-quantum-toolkit/core/pull/1237 + [#1236]: https://github.com/munich-quantum-toolkit/core/pull/1236 + [#1235]: https://github.com/munich-quantum-toolkit/core/pull/1235 + [#1232]: https://github.com/munich-quantum-toolkit/core/pull/1232 + [#1224]: https://github.com/munich-quantum-toolkit/core/pull/1224 + [#1223]: https://github.com/munich-quantum-toolkit/core/pull/1223 + [#1211]: https://github.com/munich-quantum-toolkit/core/pull/1211 + [#1210]: https://github.com/munich-quantum-toolkit/core/pull/1210 + [#1209]: https://github.com/munich-quantum-toolkit/core/pull/1209 + [#1207]: https://github.com/munich-quantum-toolkit/core/pull/1207 + [#1186]: https://github.com/munich-quantum-toolkit/core/pull/1186 + [#1181]: https://github.com/munich-quantum-toolkit/core/pull/1181 + [#1180]: https://github.com/munich-quantum-toolkit/core/pull/1180 + [#1164]: https://github.com/munich-quantum-toolkit/core/pull/1164 + [#1157]: https://github.com/munich-quantum-toolkit/core/pull/1157 + [#1151]: https://github.com/munich-quantum-toolkit/core/pull/1151 + [#1150]: https://github.com/munich-quantum-toolkit/core/pull/1150 + [#1148]: https://github.com/munich-quantum-toolkit/core/pull/1148 + [#1147]: https://github.com/munich-quantum-toolkit/core/pull/1147 + [#1140]: https://github.com/munich-quantum-toolkit/core/pull/1140 + [#1139]: https://github.com/munich-quantum-toolkit/core/pull/1139 + [#1117]: https://github.com/munich-quantum-toolkit/core/pull/1117 + [#1116]: https://github.com/munich-quantum-toolkit/core/pull/1116 + [#1108]: https://github.com/munich-quantum-toolkit/core/pull/1108 + [#1106]: https://github.com/munich-quantum-toolkit/core/pull/1106 + [#1100]: https://github.com/munich-quantum-toolkit/core/pull/1100 + [#1099]: https://github.com/munich-quantum-toolkit/core/pull/1099 + [#1098]: https://github.com/munich-quantum-toolkit/core/pull/1098 + [#1091]: https://github.com/munich-quantum-toolkit/core/pull/1091 + [#1089]: https://github.com/munich-quantum-toolkit/core/pull/1089 + [#1088]: https://github.com/munich-quantum-toolkit/core/pull/1088 + [#1076]: https://github.com/munich-quantum-toolkit/core/pull/1076 + [#1075]: https://github.com/munich-quantum-toolkit/core/pull/1075 + [#1071]: https://github.com/munich-quantum-toolkit/core/pull/1071 + [#1047]: https://github.com/munich-quantum-toolkit/core/pull/1047 + [#1042]: https://github.com/munich-quantum-toolkit/core/pull/1042 + [#1020]: https://github.com/munich-quantum-toolkit/core/pull/1020 + [#1019]: https://github.com/munich-quantum-toolkit/core/pull/1019 + [#1010]: https://github.com/munich-quantum-toolkit/core/pull/1010 + [#1001]: https://github.com/munich-quantum-toolkit/core/pull/1001 + [#996]: https://github.com/munich-quantum-toolkit/core/pull/996 + [#984]: https://github.com/munich-quantum-toolkit/core/pull/984 + [#982]: https://github.com/munich-quantum-toolkit/core/pull/982 + [#975]: https://github.com/munich-quantum-toolkit/core/pull/975 + [#973]: https://github.com/munich-quantum-toolkit/core/pull/973 + [#964]: https://github.com/munich-quantum-toolkit/core/pull/964 + [#959]: https://github.com/munich-quantum-toolkit/core/pull/959 + [#934]: https://github.com/munich-quantum-toolkit/core/pull/934 + [#933]: https://github.com/munich-quantum-toolkit/core/pull/933 + [#932]: https://github.com/munich-quantum-toolkit/core/pull/932 + [#931]: https://github.com/munich-quantum-toolkit/core/pull/931 + [#930]: https://github.com/munich-quantum-toolkit/core/pull/930 + [#926]: https://github.com/munich-quantum-toolkit/core/pull/926 + [#921]: https://github.com/munich-quantum-toolkit/core/pull/921 + [#913]: https://github.com/munich-quantum-toolkit/core/pull/913 + [#912]: https://github.com/munich-quantum-toolkit/core/pull/912 + [#911]: https://github.com/munich-quantum-toolkit/core/pull/911 + [#908]: https://github.com/munich-quantum-toolkit/core/pull/908 + [#900]: https://github.com/munich-quantum-toolkit/core/pull/900 + [#897]: https://github.com/munich-quantum-toolkit/core/pull/897 + [#895]: https://github.com/munich-quantum-toolkit/core/pull/895 + [#893]: https://github.com/munich-quantum-toolkit/core/pull/893 + [#892]: https://github.com/munich-quantum-toolkit/core/pull/892 + [#886]: https://github.com/munich-quantum-toolkit/core/pull/886 + [#885]: https://github.com/munich-quantum-toolkit/core/pull/885 + [#883]: https://github.com/munich-quantum-toolkit/core/pull/883 + [#882]: https://github.com/munich-quantum-toolkit/core/pull/882 + [#879]: https://github.com/munich-quantum-toolkit/core/pull/879 + [#878]: https://github.com/munich-quantum-toolkit/core/pull/878 + [#877]: https://github.com/munich-quantum-toolkit/core/pull/877 + [#866]: https://github.com/munich-quantum-toolkit/core/pull/866 + [#860]: https://github.com/munich-quantum-toolkit/core/pull/860 + [#859]: https://github.com/munich-quantum-toolkit/core/pull/859 + [#858]: https://github.com/munich-quantum-toolkit/core/pull/858 + [#849]: https://github.com/munich-quantum-toolkit/core/pull/849 + [#847]: https://github.com/munich-quantum-toolkit/core/pull/847 + [#846]: https://github.com/munich-quantum-toolkit/core/pull/846 + [#842]: https://github.com/munich-quantum-toolkit/core/pull/842 + [#839]: https://github.com/munich-quantum-toolkit/core/pull/839 + [#838]: https://github.com/munich-quantum-toolkit/core/pull/838 + [#832]: https://github.com/munich-quantum-toolkit/core/pull/832 + [#831]: https://github.com/munich-quantum-toolkit/core/pull/831 + [#822]: https://github.com/munich-quantum-toolkit/core/pull/822 + [#817]: https://github.com/munich-quantum-toolkit/core/pull/817 + [#810]: https://github.com/munich-quantum-toolkit/core/pull/810 + [#807]: https://github.com/munich-quantum-toolkit/core/pull/807 + [#802]: https://github.com/munich-quantum-toolkit/core/pull/802 + [#798]: https://github.com/munich-quantum-toolkit/core/pull/798 + [#789]: https://github.com/munich-quantum-toolkit/core/pull/789 + [#763]: https://github.com/munich-quantum-toolkit/core/pull/763 + [#762]: https://github.com/munich-quantum-toolkit/core/pull/762 + [#758]: https://github.com/munich-quantum-toolkit/core/pull/758 + [#741]: https://github.com/munich-quantum-toolkit/core/pull/741 + [#724]: https://github.com/munich-quantum-toolkit/core/pull/724 + [#662]: https://github.com/munich-quantum-toolkit/core/pull/662 + [#543]: https://github.com/munich-quantum-toolkit/core/pull/543 + [**@a9b7e70**]: https://github.com/munich-quantum-toolkit/core/pull/798/commits/a9b7e70aaeb532fe8e1e31a7decca86d81eb523f [**@burgholzer**]: https://github.com/burgholzer + [**@ystade**]: https://github.com/ystade + [**@DRovara**]: https://github.com/DRovara + [**@flowerthrower**]: https://github.com/flowerthrower + [**@BertiFlorea**]: https://github.com/BertiFlorea + [**@M-J-Hochreiter**]: https://github.com/M-J-Hochreiter + [**@rotmanjanez**]: https://github.com/rotmanjanez + [**@pehamTom**]: https://github.com/pehamTom + [**@MatthiasReumann**]: https://github.com/MatthiasReumann + [**@denialhaag**]: https://github.com/denialhaag + [**q-inho**]: https://github.com/q-inho + [**@li-mingbao**]: https://github.com/li-mingbao + [**@lavanya-m-k**]: https://github.com/lavanya-m-k + [**@taminob**]: https://github.com/taminob + [**@lsschmid**]: https://github.com/lsschmid + [**@marcelwa**]: https://github.com/marcelwa + [**@lirem101**]: https://github.com/lirem101 + [**@Ectras**]: https://github.com/Ectras + [**@simon1hofmann**]: https://github.com/simon1hofmann + [**@keefehuang**]: https://github.com/keefehuang + [**@J4MMlE**]: https://github.com/J4MMlE + [**@rturrado**]: https://github.com/rturrado [Keep a Changelog]: https://keepachangelog.com/en/1.1.0/ + [Common Changelog]: https://common-changelog.org + [QDMI-on-IQM]: https://github.com/iqm-finland/QDMI-on-IQM + [Semantic Versioning]: https://semver.org/spec/v2.0.0.html + [munich-quantum-toolkit]: https://github.com/munich-quantum-toolkit + [PEP 639]: https://peps.python.org/pep-0639/ + [PEP 735]: https://peps.python.org/pep-0735/ + [CMake presets]: https://cmake.org/cmake/help/latest/manual/cmake-presets.7.html + [munich-quantum-toolkit/workflows]: https://github.com/munich-quantum-toolkit/workflows diff --git a/mlir/include/mlir/Dialect/QCO/Transforms/Passes.td b/mlir/include/mlir/Dialect/QCO/Transforms/Passes.td index 5faad215e5..0b50aa7ad0 100644 --- a/mlir/include/mlir/Dialect/QCO/Transforms/Passes.td +++ b/mlir/include/mlir/Dialect/QCO/Transforms/Passes.td @@ -182,6 +182,42 @@ def MappingPass : Pass<"place-and-route", "mlir::ModuleOp"> { // Optimization Passes //===----------------------------------------------------------------------===// +def ConstantPropagation : Pass<"constant-propagation", "mlir::ModuleOp"> { + let dependentDialects = ["mlir::qco::QCODialect", + "::mlir::arith::ArithDialect"]; + let summary = "This pass applies constant propagation to a circuit. It " + "assumes that all input qubits are |0>. It propagates the " + "state of the qubits up to a given complexity threshold and " + "removes gates which are superfluous considering the current " + "state."; + let description = [{ + This pass applies quantum constant propagation. This optimization routine assumes that the input qubits of the + circuits are |0>. It propagates the qubit states and the state of additional classical values through the circuit. + All quantum instructions are removed which are superfluous considering the current state. + + The qubit states and classical values are stored in hybrid states. Hybrid states are stored in a union table to + reduce the amount of complex amplitudes and classical values to track. There is a maximum number of non zero + amplitudes that is saved per union table entry. Additionally, there is also a maximum of hybrid states that can be + propagated. If the maximum number of amplitudes or the maximum number of hybrid states is exceeded, the propagated + state reaches top and no optimization routines are further applied. + + The applied optimization routines are: + + **General Control Reduction** + If a controlling qubit is always one, the control is removed. + If a controlling qubit is always zero, the complete gate is removed. + + }]; + let options = [Option<"maximumNonzeroAmplitudes", + "maximum-nonzero-amplitudes", "std::size_t", "4", + "The maximum number of non-zero amplitudes in the " + "tracked quantum states before reaching top.">, + Option<"maximumHybridStates", "maximum-hybrid-states", + "std::size_t", "4", + "The maximum number of hybrid states which have a " + "non-zero probability.">]; +} + def PauliTwirl2QGates : Pass<"pauli-twirl-2q-gates", "mlir::ModuleOp"> { let dependentDialects = ["mlir::qco::QCODialect", "::mlir::arith::ArithDialect"]; diff --git a/mlir/lib/Dialect/QCO/Transforms/CMakeLists.txt b/mlir/lib/Dialect/QCO/Transforms/CMakeLists.txt index 621df3444e..0718d2dc74 100644 --- a/mlir/lib/Dialect/QCO/Transforms/CMakeLists.txt +++ b/mlir/lib/Dialect/QCO/Transforms/CMakeLists.txt @@ -15,10 +15,15 @@ add_mlir_library( PUBLIC MQTCompilerTarget PRIVATE + MLIRAnalysis + MLIRControlFlowInterfaces + MLIRFunctionInterfaces MLIRQCODialect MLIRQCOUtils + MLIRQTensorDialect MLIRQTensorUtils MLIRArithDialect + MLIRFuncDialect MLIRMathDialect MLIRMQTDialect MLIRMQTTransforms diff --git a/mlir/lib/Dialect/QCO/Transforms/Optimizations/ConstantPropagation.cpp b/mlir/lib/Dialect/QCO/Transforms/Optimizations/ConstantPropagation.cpp new file mode 100644 index 0000000000..543f5c1026 --- /dev/null +++ b/mlir/lib/Dialect/QCO/Transforms/Optimizations/ConstantPropagation.cpp @@ -0,0 +1,98 @@ +/* + * Copyright (c) 2023 - 2026 Chair for Design Automation, TUM + * Copyright (c) 2025 - 2026 Munich Quantum Software Company GmbH + * All rights reserved. + * + * SPDX-License-Identifier: MIT + * + * Licensed under the MIT License + */ + +#include "ConstantPropagation/ConstantPropagationAnalysis.hpp" +#include "ConstantPropagation/Decisions.hpp" +#include "ConstantPropagation/Rewriter.hpp" +#include "mlir/Dialect/MQT/IR/MQTDialect.h" +#include "mlir/Dialect/QCO/Transforms/Passes.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace mlir::qco { + +#define GEN_PASS_DEF_CONSTANTPROPAGATION +#include "mlir/Dialect/QCO/Transforms/Passes.h.inc" + +namespace { + +/** + * @brief Quantum constant propagation. + * + * Assumes every input qubit of the entry-point function starts in |0>, + * propagates the quantum/classical state through the circuit up to a complexity + * threshold (an MLIR `DenseForwardDataFlowAnalysis` over a `UnionTable` + * lattice), then removes operations that are superfluous given that state. + * + * Rewrites: delete a `qco.ctrl` whose controls can never all hold, and remove + * the always-satisfied controls from a `qco.ctrl` - rebuilding it with the + * rest, or inlining its body when every control was redundant. Analyze and + * rewrite alternate until a fixpoint because a removed gate can change a later + * gate's control facts. Classical controls are not reasoned about yet. + */ +struct ConstantPropagation final + : impl::ConstantPropagationBase { + using ConstantPropagationBase::ConstantPropagationBase; + +protected: + void runOnOperation() override { + ModuleOp module = getOperation(); + + func::FuncOp entry; + for (auto func : module.getOps()) { + if (!mqt::isEntryPoint(func)) { + continue; + } + if (entry) { + module.emitError( + "constant propagation supports a single entry-point function"); + return signalPassFailure(); + } + entry = func; + } + if (!entry) { + return; + } + + IRRewriter rewriter(&getContext()); + constexpr unsigned maxRounds = 64; + for (unsigned round = 0; round < maxRounds; ++round) { + DataFlowSolver solver; + solver.load(); + solver.load(); + solver.load(maximumNonzeroAmplitudes, + maximumHybridStates); + if (failed(solver.initializeAndRun(module))) { + return signalPassFailure(); // the analysis emitted the diagnostic + } + + const SmallVector decisions = collectDecisions(entry, solver); + if (decisions.empty()) { + return; // fixpoint reached + } + applyDecisions(decisions, rewriter); + } + + entry.emitError("constant propagation did not converge within ") + << maxRounds << " rounds"; + signalPassFailure(); + } +}; + +} // namespace + +} // namespace mlir::qco diff --git a/mlir/lib/Dialect/QCO/Transforms/Optimizations/ConstantPropagation/ConstantPropagationAnalysis.cpp b/mlir/lib/Dialect/QCO/Transforms/Optimizations/ConstantPropagation/ConstantPropagationAnalysis.cpp new file mode 100644 index 0000000000..8ca5a09b19 --- /dev/null +++ b/mlir/lib/Dialect/QCO/Transforms/Optimizations/ConstantPropagation/ConstantPropagationAnalysis.cpp @@ -0,0 +1,380 @@ +/* + * Copyright (c) 2023 - 2026 Chair for Design Automation, TUM + * Copyright (c) 2025 - 2026 Munich Quantum Software Company GmbH + * All rights reserved. + * + * SPDX-License-Identifier: MIT + * + * Licensed under the MIT License + */ + +#include "ConstantPropagationAnalysis.hpp" + +#include "UnionTable.hpp" +#include "mlir/Dialect/MQT/IR/MQTDialect.h" +#include "mlir/Dialect/QCO/IR/QCODialect.h" +#include "mlir/Dialect/QCO/IR/QCOInterfaces.h" +#include "mlir/Dialect/QCO/IR/QCOOps.h" +#include "mlir/Dialect/QCO/Utils/Matrix.h" +#include "mlir/Dialect/QTensor/IR/QTensorOps.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +namespace mlir::qco { + +/// @brief Materializes a value range into an owned vector. +template static SmallVector toVec(const Range& range) { + return {range.begin(), range.end()}; +} + +/// @brief Whether v is a qubit argument of the entry-point function's entry +/// block (so its initial state is |0>, per the pass contract). Arguments of any +/// other function have unknown provenance. +static bool isEntryPointQubitArgument(Value v) { + const auto arg = dyn_cast(v); + if (!arg || !isa(arg.getType())) { + return false; + } + Block* const block = arg.getOwner(); + return block->isEntryBlock() && block->getParentOp() != nullptr && + mqt::isEntryPoint(block->getParentOp()); +} + +/// @brief Ensures every qubit operand of op is tracked before use: an +/// entry-point argument starts in |0>, anything else of unknown provenance +/// collapses to top. +static void ensureSeeded(UnionTable& table, Operation* op) { + for (Value operand : op->getOperands()) { + if (!isa(operand.getType()) || table.isTracked(operand)) { + continue; + } + table.seedQubit(operand); + if (!isEntryPointQubitArgument(operand)) { + table.markQubitsTop(operand); + } + } +} + +/** + * Conservative fallback for an operation whose quantum effect the analysis does + * not model - a valid but unsupported construct (`scf.for`, `qco.index_switch`, + * ...) or any unrecognized op that touches qubits. + */ +static LogicalResult applyUnmodelledOp(UnionTable& table, Operation* op) { + const auto isQubit = [](Value v) { return isa(v.getType()); }; + + SmallVector qubitOperands; + for (Value operand : op->getOperands()) { + if (isQubit(operand)) { + qubitOperands.push_back(operand); + } + } + SmallVector qubitResults; + for (Value result : op->getResults()) { + if (isQubit(result)) { + qubitResults.push_back(result); + } + } + + // Thread each qubit through operand -> result. zip() pairs the common prefix + table.forwardValues(qubitOperands, qubitResults); + table.markQubitsTop(qubitResults); + return success(); +} + +//===----------------------------------------------------------------------===// +// UnionTableLattice +//===----------------------------------------------------------------------===// + +ChangeResult UnionTableLattice::join(const AbstractDenseLattice& other) { + const auto& rhs = llvm::cast(other); + if (!rhs.initialized) { + return ChangeResult::NoChange; + } + return joinUnionTable(rhs.table); +} + +ChangeResult UnionTableLattice::setUnionTable(UnionTable next) { + if (initialized && next == table) { + return ChangeResult::NoChange; + } + table = std::move(next); + initialized = true; + return ChangeResult::Change; +} + +ChangeResult UnionTableLattice::joinUnionTable(const UnionTable& rhs) { + if (!initialized) { + return setUnionTable(rhs); + } + UnionTable joined = table; + joined.join(rhs); + return setUnionTable(std::move(joined)); +} + +void UnionTableLattice::print(raw_ostream& os) const { + if (!initialized) { + os << ""; + return; + } + table.print(os); +} + +//===----------------------------------------------------------------------===// +// ConstantPropagationAnalysis +//===----------------------------------------------------------------------===// + +ConstantPropagationAnalysis::ConstantPropagationAnalysis( + DataFlowSolver& solver, size_t maxNonzeroAmplitudes, size_t maxHybridStates) + : DenseForwardDataFlowAnalysis(solver), + maxNonzeroAmplitudes(maxNonzeroAmplitudes), + maxHybridStates(maxHybridStates) {} + +LogicalResult ConstantPropagationAnalysis::initialize(Operation* top) { + // Does not reason across a call boundary: any call anywhere in the module + // makes every program point top. Uncalled helper functions are fine - the + // analysis just treats their (non-entry-point) qubit arguments as unknown. + bool hasCall = false; + top->walk([&](Operation* op) { hasCall |= isa(op); }); + bailToTop = hasCall; + return DenseForwardDataFlowAnalysis::initialize(top); +} + +UnionTable ConstantPropagationAnalysis::freshTable() const { + UnionTable table(maxNonzeroAmplitudes, maxHybridStates); + if (bailToTop) { + table.markAllTop(); + } + return table; +} + +void ConstantPropagationAnalysis::setToEntryState(UnionTableLattice* lattice) { + // Entry qubits are seeded lazily on first use (see ensureSeeded); the entry + // state is just an empty, budgeted table. + propagateIfChanged(lattice, lattice->setUnionTable(freshTable())); +} + +LogicalResult ConstantPropagationAnalysis::visitOperation( + Operation* op, const UnionTableLattice& before, UnionTableLattice* after) { + if (bailToTop) { + propagateIfChanged(after, after->setUnionTable(freshTable())); + return success(); + } + + // Region-branch ops (qco.if, qco.index_switch) are handled through + // visitRegionBranchControlFlowTransfer; nothing to do for the op itself. + if (isa(op)) { + return success(); + } + + // Bodies of qco.ctrl / qco.inv / qco.pow are interpreted by the enclosing + // modifier's handler, so their nested ops just pass the state through. + if (Operation* const parent = op->getParentOp(); + parent != nullptr && isa(parent)) { + propagateIfChanged(after, after->setUnionTable(before.getUnionTable())); + return success(); + } + + UnionTable table = before.getUnionTable(); + if (failed(applyOperation(table, op, /*quantumControls=*/{}))) { + // Valid unsupported ops are absorbed conservatively by applyOperation, so a + // failure here means a UnionTable invariant broke - an internal bug, not a + // property of the input. + return op->emitError() + << "constant propagation left the abstract state inconsistent at '" + << op->getName() << "' (internal error)"; + } + propagateIfChanged(after, after->setUnionTable(std::move(table))); + return success(); +} + +LogicalResult +ConstantPropagationAnalysis::applyOperation(UnionTable& table, Operation* op, + ArrayRef quantumControls) { + ensureSeeded(table, op); + + return TypeSwitch(op) + .Case([&](AllocOp alloc) { + table.seedQubit(alloc.getResult()); + return success(); + }) + .Case([&](StaticOp stat) { + table.seedQubit(stat.getQubit()); + return success(); + }) + .Case([&](qtensor::ExtractOp extract) { + table.seedQubit(extract.getResult()); + return success(); + }) + .Case( + [](Operation*) { return success(); }) + .Case( + [&](arith::ConstantOp constant) -> LogicalResult { + const Attribute value = constant.getValue(); + if (!isa(value)) { + return failure(); + } + table.seedClassical(constant.getResult(), value); + return success(); + }) + .Case([&](MeasureOp measure) { + return table.measureQubit(measure.getQubitIn(), measure.getQubitOut(), + measure.getResult()); + }) + .Case([&](ResetOp reset) { + return table.resetQubit(reset.getQubitIn(), reset.getQubitOut()); + }) + .Case([&](GPhaseOp gphase) { + return table.addGlobalPhase(gphase.getTheta(), quantumControls, + quantumControls); + }) + .Case( + [&](CtrlOp ctrl) { return applyCtrl(table, ctrl, quantumControls); }) + .Case([&](Operation* branch) { + // Normally routed by the framework + // (visitRegionBranchControlFlowTransfer); reaching one here means it is + // nested in a qco.ctrl / qco.inv / qco.pow body, which the analysis + // does not interpret - fall back to the conservative top. + return applyUnmodelledOp(table, branch); + }) + .Case([&](UnitaryOpInterface gate) { + // Every remaining unitary: base gates, and qco.inv / qco.pow bodies + // (which apply here when they expose a compile-time matrix, top out + // otherwise). + return applyUnitary(table, gate, quantumControls); + }) + .Default([&](Operation* other) -> LogicalResult { + // Not a QCO operation. Anything clear of qubits is a classical op to + // fold; an unrecognized qubit-touching op (e.g. scf.for) is not + // modelled, so its qubits collapse to top rather than failing the pass. + const auto isQubit = [](Type t) { return isa(t); }; + if (llvm::any_of(other->getOperandTypes(), isQubit) || + llvm::any_of(other->getResultTypes(), isQubit)) { + return applyUnmodelledOp(table, other); + } + table.propagateClassical(other); + return success(); + }); +} + +LogicalResult +ConstantPropagationAnalysis::applyUnitary(UnionTable& table, + UnitaryOpInterface gate, + ArrayRef quantumControls) { + const auto targetsIn = toVec(gate.getInputTargets()); + const auto targetsOut = toVec(gate.getOutputTargets()); + + Matrix2x2 matrix2; + if (gate.getNumTargets() == 1 && gate.getUnitaryMatrix2x2(matrix2)) { + return table.applyMatrix1Q(targetsIn[0], targetsOut[0], matrix2, + quantumControls, quantumControls); + } + Matrix4x4 matrix4; + if (gate.getNumTargets() == 2 && gate.getUnitaryMatrix4x4(matrix4)) { + return table.applyMatrix2Q(targetsIn[0], targetsIn[1], targetsOut[0], + targetsOut[1], matrix4, quantumControls, + quantumControls); + } + // Parametric-without-constant, >2-qubit, dynamic-matrix, or an unmodelled + // qco.inv / qco.pow body: the targets become top. + table.markQubitsTop(targetsIn); + table.forwardValues(targetsIn, targetsOut); + return success(); +} + +LogicalResult +ConstantPropagationAnalysis::applyCtrl(UnionTable& table, CtrlOp ctrl, + ArrayRef quantumControls) { + Block& body = ctrl.getRegion().front(); + + table.forwardValues(toVec(ctrl.getInputTargets()), + toVec(body.getArguments())); + + SmallVector innerControls(quantumControls); + llvm::append_range(innerControls, ctrl.getInputControls()); + + for (Operation& nested : body.without_terminator()) { + if (failed(applyOperation(table, &nested, innerControls))) { + return failure(); + } + } + + auto yield = cast(body.getTerminator()); + table.forwardValues(toVec(yield.getOperands()), + toVec(ctrl.getOutputTargets())); + table.forwardValues(toVec(ctrl.getInputControls()), + toVec(ctrl.getOutputControls())); + return success(); +} + +void ConstantPropagationAnalysis::visitRegionBranchControlFlowTransfer( + RegionBranchOpInterface branch, std::optional regionFrom, + std::optional regionTo, const UnionTableLattice& before, + UnionTableLattice* after) { + // nullopt = the parent op; a value = the index of one of `branch`'s regions. + auto ifOp = dyn_cast(branch.getOperation()); + if (bailToTop || !ifOp || !before.isInitialized()) { + // bailToTop, qco.index_switch, or any not-yet-modelled region-branch op: + // a plain join of `before` into `after`, without the operand->block-arg + // (enter) / yield->result (leave) renaming that Case A / Case C do below. + // The renamed value is thus untracked downstream, so the next + // applyOperation that consumes it runs ensureSeeded, which - the value not + // being an entry-point argument - marks its qubits top. (Under bailToTop + // `before` is already all-top; an uninitialized `before` makes the join a + // no-op.) + DenseForwardDataFlowAnalysis::visitRegionBranchControlFlowTransfer( + branch, regionFrom, regionTo, before, after); + return; + } + + UnionTable table = before.getUnionTable(); + + if (!regionFrom.has_value() && regionTo.has_value()) { + // Entering a branch: the op's linear operands become the region's block + // arguments. Both then and else regions get the same incoming state; a + // constant condition is exploited at rewrite time. + Block& body = ifOp->getRegion(*regionTo).front(); + table.forwardValues(toVec(ifOp.getQubits()), toVec(body.getArguments())); + propagateIfChanged(after, after->setUnionTable(std::move(table))); + return; + } + + if (!regionFrom.has_value() || regionTo.has_value()) { + // Happens if loops produce region -> region calls (not supported) or a + // region is empty (e.g. an else branch does not exist) + return; + } + + // Leaving a branch: the region's yield becomes the op's results. Classical + // results precede the linear ones. Each region contributes one exit edge; the + // lattice accumulates them via join. + auto yield = + cast(ifOp->getRegion(*regionFrom).front().getTerminator()); + const size_t numClassical = ifOp.getClassicalResults().size(); + table.forwardValues(toVec(yield.getOperands().take_front(numClassical)), + toVec(ifOp.getClassicalResults())); + table.forwardValues(toVec(yield.getOperands().drop_front(numClassical)), + toVec(ifOp.getLinearResults())); + propagateIfChanged(after, after->joinUnionTable(table)); +} + +} // namespace mlir::qco diff --git a/mlir/lib/Dialect/QCO/Transforms/Optimizations/ConstantPropagation/ConstantPropagationAnalysis.hpp b/mlir/lib/Dialect/QCO/Transforms/Optimizations/ConstantPropagation/ConstantPropagationAnalysis.hpp new file mode 100644 index 0000000000..4722a68e20 --- /dev/null +++ b/mlir/lib/Dialect/QCO/Transforms/Optimizations/ConstantPropagation/ConstantPropagationAnalysis.hpp @@ -0,0 +1,142 @@ +/* + * Copyright (c) 2023 - 2026 Chair for Design Automation, TUM + * Copyright (c) 2025 - 2026 Munich Quantum Software Company GmbH + * All rights reserved. + * + * SPDX-License-Identifier: MIT + * + * Licensed under the MIT License + */ + +#pragma once + +#include "UnionTable.hpp" +#include "mlir/Dialect/QCO/IR/QCOInterfaces.h" +#include "mlir/Dialect/QCO/IR/QCOOps.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +namespace mlir::qco { + +/** + * @brief The dense-lattice payload for the constant-propagation analysis: one + * UnionTable per program point. + * + * The lattice has an explicit *uninitialized* (bottom) state so that the + * framework's join-accumulation over control-flow edges works: joining bottom + * with a value adopts the value; joining two values delegates to + * UnionTable::join. + */ +class UnionTableLattice : public dataflow::AbstractDenseLattice { + UnionTable table; + bool initialized = false; + +public: + MLIR_DEFINE_EXPLICIT_INTERNAL_INLINE_TYPE_ID(UnionTableLattice) + + using AbstractDenseLattice::AbstractDenseLattice; + + /// @brief LLVM-RTTI hook. The dataflow solver instantiates exactly one dense + /// lattice type per analysis, so every AbstractDenseLattice that is handed + /// (e.g. in join) is a UnionTableLattice. + static bool classof(const AbstractDenseLattice*) { return true; } + + ChangeResult join(const AbstractDenseLattice& other) override; + + void print(raw_ostream& os) const override; + + [[nodiscard("UnionTableLattice::isInitialized is called but ignored")]] bool + isInitialized() const { + return initialized; + } + + /// @brief The payload. Only meaningful once @isInitialized. + [[nodiscard("UnionTableLattice::getUnionTable is called but " + "ignored")]] const UnionTable& + getUnionTable() const { + return table; + } + + /// @brief Replaces the payload; returns whether it changed. + ChangeResult setUnionTable(UnionTable next); + + /// @brief Joins rhs into the payload (adopting it if still uninitialized). + ChangeResult joinUnionTable(const UnionTable& rhs); +}; + +/** + * Forward dense data-flow analysis that threads a UnionTable through a QCO + * program, interpreting gates, measurements, resets, global phases, classical + * folds, control modifiers, and constant/branching `qco.if`. + * + * Unsupported constructs (`scf.for`, `qco.index_switch`, and any operation + * touching qubits that the analysis cannot model) are handled conservatively: + * the qubits they consume or produce collapse to top, everything else keeps its + * state. Precision losses (parametric gates, `qco.inv` / `qco.pow` bodies, + * non-constant `qco.if`) collapse the affected qubits the same way. The pass + * only fails (via `emitError`) if a `UnionTable` invariant breaks - an internal + * bug rather than a property of the input. + * + * Does not call across a boundary: if the module contains any call, the + * analysis reports top everywhere. Uncalled helper functions are tolerated - + * only the entry-point function's qubit arguments are assumed to be |0>; every + * other function's qubits are treated as unknown. + */ +class ConstantPropagationAnalysis + : public dataflow::DenseForwardDataFlowAnalysis { + size_t maxNonzeroAmplitudes; + size_t maxHybridStates; + + /// @brief Set in @ref initialize when the module contains any call; every + /// program point is then top. + bool bailToTop = false; + + /// @brief A budgeted empty table, or an all-top one when @ref bailToTop. + [[nodiscard]] UnionTable freshTable() const; + + /// @brief Dispatches a single operation onto table, given the quantum + /// controls accumulated by any enclosing qco.ctrl. + LogicalResult applyOperation(UnionTable& table, Operation* op, + ArrayRef quantumControls); + + /// @brief Applies a unitary operation: its 1-/2-qubit matrix if available, + /// otherwise the targets become top (parametric, >2-qubit, dynamic-matrix, or + /// an unmodelled qco.inv / qco.pow body). + static LogicalResult applyUnitary(UnionTable& table, UnitaryOpInterface gate, + ArrayRef quantumControls); + + /// @brief Interprets a qco.ctrl body, extending the control context. + LogicalResult applyCtrl(UnionTable& table, CtrlOp ctrl, + ArrayRef quantumControls); + +protected: + void setToEntryState(UnionTableLattice* lattice) override; + +public: + ConstantPropagationAnalysis(DataFlowSolver& solver, + size_t maxNonzeroAmplitudes, + size_t maxHybridStates); + + LogicalResult initialize(Operation* top) override; + + LogicalResult visitOperation(Operation* op, const UnionTableLattice& before, + UnionTableLattice* after) override; + + void visitRegionBranchControlFlowTransfer(RegionBranchOpInterface branch, + std::optional regionFrom, + std::optional regionTo, + const UnionTableLattice& before, + UnionTableLattice* after) override; +}; + +} // namespace mlir::qco diff --git a/mlir/lib/Dialect/QCO/Transforms/Optimizations/ConstantPropagation/Decisions.hpp b/mlir/lib/Dialect/QCO/Transforms/Optimizations/ConstantPropagation/Decisions.hpp new file mode 100644 index 0000000000..314ee41606 --- /dev/null +++ b/mlir/lib/Dialect/QCO/Transforms/Optimizations/ConstantPropagation/Decisions.hpp @@ -0,0 +1,50 @@ +/* + * Copyright (c) 2023 - 2026 Chair for Design Automation, TUM + * Copyright (c) 2025 - 2026 Munich Quantum Software Company GmbH + * All rights reserved. + * + * SPDX-License-Identifier: MIT + * + * Licensed under the MIT License + */ + +#pragma once + +#include "mlir/Dialect/QCO/IR/QCOOps.h" + +#include + +#include + +namespace mlir::qco { + +/** + * A controlled gate whose control configuration can never be satisfied in the + * current state: the body never runs, so the whole CtrlOp is deleted and every + * qubit passes straight through. + */ +struct DropOp { + CtrlOp op; +}; + +/** + * @brief A controlled gate and the control qubits that provably always hold in + * the current state. + * + * If a real control remains, the op is rebuilt with only those. If + * dropControlIndices covers *every* control, the gate runs unconditionally and + * its body is inlined in place of the op. + * + * dropControlIndices indexes into op.getInputControls(). Indices, not values, + * so an earlier rewrite in the same batch cannot invalidate the decision. + * Classical controls are not considered yet. + */ +struct StripControls { + CtrlOp op; + SmallVector dropControlIndices; +}; + +/// @brief One rewrite the pass has decided to perform. +using Decision = std::variant; + +} // namespace mlir::qco diff --git a/mlir/lib/Dialect/QCO/Transforms/Optimizations/ConstantPropagation/HybridState.cpp b/mlir/lib/Dialect/QCO/Transforms/Optimizations/ConstantPropagation/HybridState.cpp new file mode 100644 index 0000000000..e8c956b723 --- /dev/null +++ b/mlir/lib/Dialect/QCO/Transforms/Optimizations/ConstantPropagation/HybridState.cpp @@ -0,0 +1,429 @@ +/* + * Copyright (c) 2023 - 2026 Chair for Design Automation, TUM + * Copyright (c) 2025 - 2026 Munich Quantum Software Company GmbH + * All rights reserved. + * + * SPDX-License-Identifier: MIT + * + * Licensed under the MIT License + */ + +#include "HybridState.hpp" + +#include "QuantumState.hpp" +#include "mlir/Dialect/QCO/Utils/Matrix.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +namespace mlir::qco { + +/// @brief Truthiness of a resolved classical constant (non-zero == true), or +/// nullopt if attr is not an integer/index/bool/float constant. +static std::optional classicalTruth(Attribute attr) { + if (const auto ia = dyn_cast_if_present(attr)) { + return !ia.getValue().isZero(); + } + if (const auto fa = dyn_cast_if_present(attr)) { + return !fa.getValue().isZero(); + } + return std::nullopt; +} + +/// @brief Whether every quantum control qubit is present in the group. A +/// missing quantum control is a caller/propagation bug that must fail the gate +/// even when the classical controls would skip its application. +static bool quantumControlsPresent(const QuantumState& state, + ArrayRef quantumCtrlsIn) { + return llvm::all_of(quantumCtrlsIn, + [&](Value qc) { return state.contains(qc); }); +} + +/// @brief Numeric value of a resolved classical constant, or nullopt if attr is +/// not an integer/index/bool/float constant. +static std::optional classicalDouble(Attribute attr) { + if (const auto ia = dyn_cast_if_present(attr)) { + if (const std::optional v = ia.getValue().trySExtValue()) { + return static_cast(*v); + } + return {}; + } + if (const auto fa = dyn_cast_if_present(attr)) { + return fa.getValueAsDouble(); + } + return {}; +} + +//===----------------------------------------------------------------------===// +// Observers +//===----------------------------------------------------------------------===// + +std::optional HybridState::getClassical(Value v) const { + const auto it = classical.find(v); + if (it == classical.end()) { + return std::nullopt; + } + return it->second; +} + +//===----------------------------------------------------------------------===// +// Mutation +//===----------------------------------------------------------------------===// + +void HybridState::setClassical(Value v, Attribute attr) { classical[v] = attr; } + +void HybridState::forwardValue(Value from, Value to) { + state.forwardQubit(from, to); + const auto it = classical.find(from); + if (it != classical.end()) { + const Attribute attr = it->second; + classical.erase(it); + classical[to] = attr; + } +} + +void HybridState::markStateTop() { state.markTop(); } + +void HybridState::intersectClassical(const HybridState& other) { + SmallVector disagreeing; + for (const auto& [v, attr] : classical) { + const auto it = other.classical.find(v); + if (it == other.classical.end() || it->second != attr) { + disagreeing.push_back(v); + } + } + for (Value v : disagreeing) { + classical.erase(v); + } +} + +HybridState HybridState::tensor(const HybridState& other) const { + HybridState result(state.unify(other.state), maxNonzeroAmplitudes, + probability * other.probability); + result.globalPhase = globalPhase * other.globalPhase; + result.classical = classical; + for (const auto& [v, attr] : other.classical) { + result.classical[v] = attr; + } + return result; +} + +//===----------------------------------------------------------------------===// +// Classical-control handling +//===----------------------------------------------------------------------===// + +FailureOr HybridState::classicalControlsHold(ArrayRef pos, + ArrayRef neg) const { + for (Value p : pos) { + const auto attr = getClassical(p); + if (!attr) { + return failure(); + } + const auto truth = classicalTruth(*attr); + if (!truth) { + return failure(); + } + if (!*truth) { + return false; + } + } + for (Value n : neg) { + const auto attr = getClassical(n); + if (!attr) { + return failure(); + } + const auto truth = classicalTruth(*attr); + if (!truth) { + return failure(); + } + if (*truth) { + return false; + } + } + return true; +} + +//===----------------------------------------------------------------------===// +// Gate application +//===----------------------------------------------------------------------===// + +LogicalResult HybridState::applyMatrix1Q(Value in, Value out, + const Matrix2x2& matrix, + ArrayRef quantumCtrlsIn, + ArrayRef quantumCtrlsOut, + ArrayRef posClassicalCtrls, + ArrayRef negClassicalCtrls) { + if (quantumCtrlsIn.size() != quantumCtrlsOut.size() || !state.contains(in) || + !quantumControlsPresent(state, quantumCtrlsIn)) { + return failure(); + } + const auto hold = classicalControlsHold(posClassicalCtrls, negClassicalCtrls); + if (failed(hold)) { + return failure(); + } + if (*hold) { + return state.applyMatrix1Q(in, out, matrix, quantumCtrlsIn, + quantumCtrlsOut); + } + // Classical control false: only the identities thread on. + state.forwardQubit(in, out); + state.forwardQubits(quantumCtrlsIn, quantumCtrlsOut); + return success(); +} + +LogicalResult HybridState::applyMatrix2Q(Value in0, Value in1, Value out0, + Value out1, const Matrix4x4& matrix, + ArrayRef quantumCtrlsIn, + ArrayRef quantumCtrlsOut, + ArrayRef posClassicalCtrls, + ArrayRef negClassicalCtrls) { + if (quantumCtrlsIn.size() != quantumCtrlsOut.size() || in0 == in1 || + !state.contains(in0) || !state.contains(in1) || + !quantumControlsPresent(state, quantumCtrlsIn)) { + return failure(); + } + const auto hold = classicalControlsHold(posClassicalCtrls, negClassicalCtrls); + if (failed(hold)) { + return failure(); + } + if (*hold) { + return state.applyMatrix2Q(in0, in1, out0, out1, matrix, quantumCtrlsIn, + quantumCtrlsOut); + } + state.forwardQubit(in0, out0); + state.forwardQubit(in1, out1); + state.forwardQubits(quantumCtrlsIn, quantumCtrlsOut); + return success(); +} + +LogicalResult HybridState::addGlobalPhase(Value theta, + ArrayRef quantumCtrlsIn, + ArrayRef quantumCtrlsOut, + ArrayRef posClassicalCtrls, + ArrayRef negClassicalCtrls) { + if (quantumCtrlsIn.size() != quantumCtrlsOut.size() || + !quantumControlsPresent(state, quantumCtrlsIn)) { + return failure(); + } + const auto hold = classicalControlsHold(posClassicalCtrls, negClassicalCtrls); + if (failed(hold)) { + return failure(); + } + if (*hold) { + const auto angle = classicalDouble(classical.lookup(theta)); + if (!angle) { + return failure(); + } + if (!quantumCtrlsIn.empty()) { + return state.applyControlledPhase(*angle, quantumCtrlsIn, + quantumCtrlsOut); + } + globalPhase *= std::exp(Complex{0.0, *angle}); + return success(); + } + state.forwardQubits(quantumCtrlsIn, quantumCtrlsOut); + return success(); +} + +void HybridState::propagateClassical(Operation* op) { + SmallVector operands; + operands.reserve(op->getNumOperands()); + for (Value operand : op->getOperands()) { + operands.push_back(classical.lookup(operand)); + } + SmallVector folded; + if (failed(op->fold(operands, folded)) || + folded.size() != op->getNumResults()) { + return; + } + for (const auto& [result, foldResult] : llvm::zip(op->getResults(), folded)) { + if (const auto attr = dyn_cast(foldResult)) { + setClassical(result, attr); + } + } +} + +//===----------------------------------------------------------------------===// +// Measurement / reset +//===----------------------------------------------------------------------===// + +LogicalResult HybridState::measureQubit(Value in, Value out, + Value classicalResult, + ArrayRef posClassicalCtrls, + ArrayRef negClassicalCtrls) { + if (!state.contains(in)) { + return failure(); + } + const auto hold = classicalControlsHold(posClassicalCtrls, negClassicalCtrls); + if (failed(hold)) { + return failure(); + } + if (*hold) { + auto branches = state.measure(in, out); + if (failed(branches)) { + return failure(); + } + if (branches->size() == 1) { + const auto resultType = dyn_cast(classicalResult.getType()); + if (!resultType) { + return failure(); + } + setClassical(classicalResult, + IntegerAttr::get(resultType, branches->front().bit)); + state = std::move(*branches->front().state); + return success(); + } + if (branches->size() == 2) { + state.markTop(); // This will be handled in a later version + } + // branches->empty() => state was already top. + } + state.forwardQubit(in, out); + return success(); +} + +LogicalResult HybridState::resetQubit(Value in, Value out, + ArrayRef posClassicalCtrls, + ArrayRef negClassicalCtrls) { + if (!state.contains(in)) { + return failure(); + } + const auto hold = classicalControlsHold(posClassicalCtrls, negClassicalCtrls); + if (failed(hold)) { + return failure(); + } + if (*hold) { + auto branches = state.reset(in, out); + if (failed(branches)) { + return failure(); + } + // One outcome, or two that agree = `in` was unentangled: reset is exact and + // the branch state (already named `out`) is the result. Two that disagree = + // the reduced state after tracing out `in` is mixed. + if (branches->size() == 1) { + state = std::move(*branches->front().state); + return success(); + } + if (branches->size() == 2) { + state.markTop(); + } + // branches->empty() => state was already top. + } + state.forwardQubit(in, out); + return success(); +} + +//===----------------------------------------------------------------------===// +// Queries +//===----------------------------------------------------------------------===// + +bool HybridState::isQubitAlwaysZero(Value q) const { + return state.isAlwaysZero(q); +} + +bool HybridState::isQubitAlwaysOne(Value q) const { + return state.isAlwaysOne(q); +} + +bool HybridState::isClassicalTrue(Value v) const { + const auto attr = getClassical(v); + return attr && classicalTruth(*attr).value_or(false); +} + +bool HybridState::isClassicalFalse(Value v) const { + const auto attr = getClassical(v); + if (!attr) { + return false; + } + const auto truth = classicalTruth(*attr); + return truth && !*truth; +} + +bool HybridState::areControlsSatisfiable( + ArrayRef quantumCtrls, ArrayRef posClassicalCtrls, + ArrayRef negClassicalCtrls) const { + for (Value pc : posClassicalCtrls) { + if (isClassicalFalse(pc)) { + return false; + } + } + for (Value nc : negClassicalCtrls) { + if (isClassicalTrue(nc)) { + return false; + } + } + if (quantumCtrls.empty()) { + return true; + } + SmallVector> assignment; + for (Value qc : quantumCtrls) { + if (!state.contains(qc)) { + return false; + } + assignment.emplace_back(qc, true); + } + return !state.hasAlwaysZeroAmplitude(assignment); +} + +//===----------------------------------------------------------------------===// +// Comparison / dump +//===----------------------------------------------------------------------===// + +bool HybridState::sameConfiguration(const HybridState& other) const { + if (std::abs(globalPhase - other.globalPhase) > MATRIX_TOLERANCE || + classical.size() != other.classical.size() || state != other.state) { + return false; + } + return llvm::all_of(classical, [&](const auto& entry) { + const auto it = other.classical.find(entry.first); + return it != other.classical.end() && it->second == entry.second; + }); +} + +bool HybridState::operator==(const HybridState& other) const { + return std::abs(probability - other.probability) <= MATRIX_TOLERANCE && + sameConfiguration(other); +} + +void HybridState::print(raw_ostream& os) const { + os << "p=" << llvm::format("%.4f", probability); + if (std::abs(globalPhase - Complex{1.0, 0.0}) > MATRIX_TOLERANCE) { + os << " phase=(" << llvm::format("%.4f", globalPhase.real()) << "," + << llvm::format("%.4f", globalPhase.imag()) << ")"; + } + os << " ["; + state.print(os); + os << "]"; + if (!classical.empty()) { + os << " classical:"; + SmallVector entries; + entries.reserve(classical.size()); + for (const auto& [v, attr] : classical) { + std::string entry; + llvm::raw_string_ostream entryOs(entry); + entryOs << v << "=" << attr; + entries.push_back(std::move(entry)); + } + llvm::sort(entries); + for (const auto& entry : entries) { + os << " " << entry; + } + } +} + +} // namespace mlir::qco diff --git a/mlir/lib/Dialect/QCO/Transforms/Optimizations/ConstantPropagation/HybridState.hpp b/mlir/lib/Dialect/QCO/Transforms/Optimizations/ConstantPropagation/HybridState.hpp new file mode 100644 index 0000000000..a8b64612b8 --- /dev/null +++ b/mlir/lib/Dialect/QCO/Transforms/Optimizations/ConstantPropagation/HybridState.hpp @@ -0,0 +1,355 @@ +/* + * Copyright (c) 2023 - 2026 Chair for Design Automation, TUM + * Copyright (c) 2025 - 2026 Munich Quantum Software Company GmbH + * All rights reserved. + * + * SPDX-License-Identifier: MIT + * + * Licensed under the MIT License + */ + +#pragma once + +#include "QuantumState.hpp" + +#include +#include +#include +#include +#include + +#include + +namespace mlir::qco { + +/** + * @brief One correlated subsystem-alternative. + * + * Holds a single QuantumState (one entanglement group's qubits, possibly none) + * together with the classical values correlated with it, this branch's + * probability, and its accumulated globalPhase. + * + * The enclosing UnionTable owns the partition: two HybridStates, whose qubit + * sets are disjoint, are tensor factors, two whose qubit sets are equal are + * alternatives of one probabilistic disjunction. A HybridState never sees a + * qubit outside its own state. + * + * Every mutating operation accepts positive and negative classical controls; if + * they do not hold in this branch, the operation is skipped (the qubit renames + * still happen). An unresolved classical control is a failure(). + */ +class HybridState { + size_t maxNonzeroAmplitudes; + double probability; + Complex globalPhase{1.0, 0.0}; + QuantumState state; + llvm::DenseMap classical; + + /** Whether the classical controls permit the operation in this branch: + * failure() if any is unresolved, else true (apply) / false (skip). + * @param pos The classical values that need to be true (i.e., nonzero). + * @param neg The classical values that need to be false (i.e., zero). + * + * @returns failure if one of the given Values is not present in the state. + * Success and whether the controls hold if all values are present. + */ + [[nodiscard( + "HybridState::classicalControlsHold called but ignored")]] FailureOr + classicalControlsHold(ArrayRef pos, ArrayRef neg) const; + +public: + /** + * @param state The quantum state of this subsystem (may hold no qubits). + * @param maxNonzeroAmplitudes Budget for QuantumStates created here (reset). + * @param probability This alternative's weight within its slot (1 if sole). + */ + HybridState(QuantumState state, size_t maxNonzeroAmplitudes, + double probability) + : maxNonzeroAmplitudes(maxNonzeroAmplitudes), probability(probability), + state(std::move(state)) {} + + //===--------------------------------------------------------------------===// + // Observers + //===--------------------------------------------------------------------===// + + /// @brief Whether the quantum state of this branch is top. + [[nodiscard("HybridState::isTop called but ignored")]] bool isTop() const { + return state.isTop(); + } + + [[nodiscard("HybridState::getProbability called but ignored")]] double + getProbability() const { + return probability; + } + [[nodiscard("HybridState::getGlobalPhase called but ignored")]] Complex + getGlobalPhase() const { + return globalPhase; + } + // @brief The qubits this subsystem covers (the UnionTable's partition unit). + [[nodiscard("HybridState::getQubits called but ignored")]] ArrayRef + getQubits() const { + return state.getQubits(); + } + [[nodiscard("HybridState::hasQubit called but ignored")]] bool + hasQubit(Value q) const { + return state.contains(q); + } + [[nodiscard("HybridState::getClassical called but ignored")]] + std::optional getClassical(Value v) const; + + //===--------------------------------------------------------------------===// + // Mutation + //===--------------------------------------------------------------------===// + + /** + * @brief Records the resolved constant of a classical value (overwrites). + * + * @param v The value whose attribute is changed. + * @param attr The new attribute for v. + */ + void setClassical(Value v, Attribute attr); + + /** + * @brief Renames from to to, whether it is this branch's qubit or one of its + * classical keys. No-op if from is not present. + * + * @param from The value being replaced. + * @param to The value it is replaced with. + */ + void forwardValue(Value from, Value to); + + /** + * @brief Multiplies this branch's probability by factor. + * + * @param factor The factor to multiply the probability with. + */ + void scaleProbability(double factor) { probability *= factor; } + + /** + * @brief Sets this branch's probability (its weight within its slot). + * + * @param newProbability The new probability. + */ + void setProbability(double newProbability) { probability = newProbability; } + + /// @brief Collapses this branch's QuantumState to top; classical facts stay. + void markStateTop(); + + /** + * @brief Drops every classical fact other does not hold identically. + * + * Used to build a sound representative when a disjunction of alternatives is + * collapsed: only the facts every alternative agrees on may be kept. + * + * @param other The branch to intersect this one's classical facts with. + */ + void intersectClassical(const HybridState& other); + + /** + * @brief Combines this subsystem with a disjoint one into a single + * HybridState. + * + * The qubit sets must be disjoint. Probabilities and global phases multiply; + * classical maps merge (other wins on a key collision). Becomes top if the + * tensor product exceeds the amplitude budget. + * + * @param other The Hybrid state to merge this HybridState with. + */ + [[nodiscard("HybridState::tensor called but ignored")]] HybridState + tensor(const HybridState& other) const; + + //===--------------------------------------------------------------------===// + // Gate application + //===--------------------------------------------------------------------===// + + /** + * @brief Applies a single-qubit unitary to in (renamed to out). + * + * in and all quantum controls must already be in this branch's state. + * quantumCtrlsOut, if non-empty, matches quantumCtrlsIn in size and gives the + * controls' post-gate values. + * + * @param in The qubit to apply the matrix to. + * @param out The qubit that in is changed to. + * @param matrix The matrix to apply to the amplitudes of in. + * @param quantumCtrlsIn The qubits that have to be |1> to apply the matrix. + * @param quantumCtrlsOut The qubits that quantumCtrlsIn are changed to. + * @param posClassicalCtrls The classical values that have to be true + * (nonzero) to apply the matrix. + * @param negClassicalCtrls The classical values that have to be false (zero) + * to apply the matrix. + * @return failure() if a target/control qubit is not in this state, the + * control in/out lengths mismatch, or a classical control is unresolved. + */ + [[nodiscard("HybridState::applyMatrix1Q called but ignored")]] + LogicalResult applyMatrix1Q(Value in, Value out, const Matrix2x2& matrix, + ArrayRef quantumCtrlsIn = {}, + ArrayRef quantumCtrlsOut = {}, + ArrayRef posClassicalCtrls = {}, + ArrayRef negClassicalCtrls = {}); + + /** + * @brief Applies a two-qubit unitary to in (renamed to out). + * + * in0, in1, and all quantum controls must already be in this branch's state. + * quantumCtrlsOut, if non-empty, matches quantumCtrlsIn in size and gives the + * controls' post-gate values. + * + * @param in0 The high qubit to apply the matrix to. + * @param in1 The low qubit to apply the matrix to. + * @param out0 The qubit that in0 is changed to. + * @param out1 The qubit that in1 is changed to. + * @param matrix The matrix to apply to the amplitudes of in. + * @param quantumCtrlsIn The qubits that have to be |1> to apply the matrix. + * @param quantumCtrlsOut The qubits that quantumCtrlsIn are changed to. + * @param posClassicalCtrls The classical values that have to be true + * (nonzero) to apply the matrix. + * @param negClassicalCtrls The classical values that have to be false (zero) + * to apply the matrix. + * @return failure() if a target/control qubit is not in this state, the + * control in/out lengths mismatch, or a classical control is unresolved. + */ + [[nodiscard("HybridState::applyMatrix2Q called but ignored")]] + LogicalResult applyMatrix2Q(Value in0, Value in1, Value out0, Value out1, + const Matrix4x4& matrix, + ArrayRef quantumCtrlsIn = {}, + ArrayRef quantumCtrlsOut = {}, + ArrayRef posClassicalCtrls = {}, + ArrayRef negClassicalCtrls = {}); + + /** + * @brief Adds a global phase exp(i*theta), where theta is a classical value + * resolved from this branch's constants. + * + * Uncontrolled: accumulated into globalPhase. With quantum controls: a + * relative phase on the subspace where every control is |1>. + * + * @param theta The classical value holding the rotation angle in radians. + * @param quantumCtrlsIn The qubits that have to be |1> to apply the phase. + * @param quantumCtrlsOut The qubits that quantumCtrlsIn are changed to. + * @param posClassicalCtrls The classical values that have to be true + * (nonzero) to apply the phase. + * @param negClassicalCtrls The classical values that have to be false (zero) + * to apply the phase. + * @return failure() if a control qubit is not in this state, a classical + * control is unresolved, or - when the phase would apply - theta is not a + * resolved constant (each indicates a propagation bug). + */ + [[nodiscard("HybridState::addGlobalPhase called but ignored")]] + LogicalResult addGlobalPhase(Value theta, ArrayRef quantumCtrlsIn = {}, + ArrayRef quantumCtrlsOut = {}, + ArrayRef posClassicalCtrls = {}, + ArrayRef negClassicalCtrls = {}); + + /** + * @brief Folds a classical operation using this branch's resolved constants + * and records any constant results. + * + * Operands not resolved in this branch are passed to the folder as unknown; a + * result that does not fold to a constant is left untracked. + * + * @param op The classical operation to fold (its operands and results). + */ + void propagateClassical(Operation* op); + + //===--------------------------------------------------------------------===// + // Measurement / reset + //===--------------------------------------------------------------------===// + + /** + * @brief Measures in (renamed to out), recording the outcome in + * classicalResult. + * + * If in is deterministic, classicalResult is set to the exact i1 value; + * otherwise the state is marked top, and classicalResult left unknown. + * + * @param in The qubit to be measured, + * @param out The value to change in to. + * @param classicalResult The classical value to save the result of the + * measurement in. + * @param posClassicalCtrls The classical values that have to be true + * (nonzero) to apply the matrix. + * @param negClassicalCtrls The classical values that have to be false (zero) + * to apply the matrix. + * @return failure() if in is not in this state or a classical control is + * unresolved. + */ + [[nodiscard("HybridState::measureQubit called but ignored")]] + LogicalResult measureQubit(Value in, Value out, Value classicalResult, + ArrayRef posClassicalCtrls = {}, + ArrayRef negClassicalCtrls = {}); + + /** + * @brief Resets in to |0> (renamed to out). + * + * Exact when this state is deterministic; otherwise the state is marked top + * (the reduced state is mixed). + * + * @param in The qubit to be measured, + * @param out The value to change in to. + * @param posClassicalCtrls The classical values that have to be true + * (nonzero) to apply the matrix. + * @param negClassicalCtrls The classical values that have to be false (zero) + * to apply the matrix. + * @return failure() if in is not in this state or a classical control is + * unresolved. + */ + [[nodiscard("HybridState::resetQubit called but ignored")]] LogicalResult + resetQubit(Value in, Value out, ArrayRef posClassicalCtrls = {}, + ArrayRef negClassicalCtrls = {}); + + //===--------------------------------------------------------------------===// + // Queries + //===--------------------------------------------------------------------===// + + [[nodiscard("HybridState::isQubitAlwaysZero called but ignored")]] bool + isQubitAlwaysZero(Value q) const; + [[nodiscard("HybridState::isQubitAlwaysOne called but ignored")]] bool + isQubitAlwaysOne(Value q) const; + + /// @brief Whether v is a known non-zero classical constant in this branch. + [[nodiscard("HybridState::isClassicalTrue called but ignored")]] bool + isClassicalTrue(Value v) const; + /// @brief Whether v is a known zero classical constant in this branch. + [[nodiscard("HybridState::isClassicalFalse called but ignored")]] bool + isClassicalFalse(Value v) const; + + /** + * Whether the given controls can all hold simultaneously in this branch + * (positive classical not provably false, negative not provably true, quantum + * controls jointly possible). + * + * @param quantumCtrls The qubits that have to be |1>. + * @param posClassicalCtrls The classical values that have to be true + * (nonzero) to apply the matrix. + * @param negClassicalCtrls The classical values that have to be false (zero) + * to apply the matrix. + */ + [[nodiscard("HybridState::areControlsSatisfiable called but ignored")]] bool + areControlsSatisfiable(ArrayRef quantumCtrls, + ArrayRef posClassicalCtrls, + ArrayRef negClassicalCtrls) const; + + //===--------------------------------------------------------------------===// + // Comparison / dump + //===--------------------------------------------------------------------===// + + /** + * Whether the two branches carry the same state, global phase, and classical + * facts - everything except their probability. The de-dup key when merging + * alternatives in UnionTable::join. + * + * @param other The HybridState to compare this one with. + */ + [[nodiscard("HybridState::sameConfiguration called but ignored")]] bool + sameConfiguration(const HybridState& other) const; + + /// @brief sameConfiguration and equal probability, both within + /// MATRIX_TOLERANCE. + [[nodiscard("HybridState::== called but ignored")]] bool + operator==(const HybridState& other) const; + + void print(raw_ostream& os) const; +}; + +} // namespace mlir::qco diff --git a/mlir/lib/Dialect/QCO/Transforms/Optimizations/ConstantPropagation/QuantumState.cpp b/mlir/lib/Dialect/QCO/Transforms/Optimizations/ConstantPropagation/QuantumState.cpp new file mode 100644 index 0000000000..9763154f8a --- /dev/null +++ b/mlir/lib/Dialect/QCO/Transforms/Optimizations/ConstantPropagation/QuantumState.cpp @@ -0,0 +1,431 @@ +/* + * Copyright (c) 2023 - 2026 Chair for Design Automation, TUM + * Copyright (c) 2025 - 2026 Munich Quantum Software Company GmbH + * All rights reserved. + * + * SPDX-License-Identifier: MIT + * + * Licensed under the MIT License + */ + +#include "QuantumState.hpp" + +#include "mlir/Dialect/QCO/Utils/Matrix.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +namespace mlir::qco { + +namespace { +/// Largest number of qubits a group can track (we use uint_64t as datatype). +constexpr unsigned MAX_GROUP_QUBITS = 63; +} // namespace + +QuantumState::QuantumState(ArrayRef qubits, size_t maxNonzeroAmplitudes) + : maxNonzeroAmplitudes(maxNonzeroAmplitudes), + qubits(qubits.begin(), qubits.end()) { + if (qubits.size() > MAX_GROUP_QUBITS) { + markTop(); + return; + } + amplitudes[0] = Complex{1.0, 0.0}; +} + +QuantumState QuantumState::singletonZero(Value qubit, + size_t maxNonzeroAmplitudes) { + return {ArrayRef(qubit), maxNonzeroAmplitudes}; +} + +std::optional QuantumState::indexOf(Value q) const { + for (const auto [idx, qubit] : llvm::enumerate(qubits)) { + if (qubit == q) { + return static_cast(idx); + } + } + return std::nullopt; +} + +uint64_t QuantumState::maskOf(ArrayRef values) const { + uint64_t mask = 0; + for (Value v : values) { + if (const auto idx = indexOf(v)) { + mask |= uint64_t{1} << *idx; + } + } + return mask; +} + +void QuantumState::markTop() { + top = true; + amplitudes.clear(); +} + +void QuantumState::forwardQubit(Value from, Value to) { + if (const auto idx = indexOf(from)) { + qubits[*idx] = to; + } +} + +void QuantumState::forwardQubits(ArrayRef from, ArrayRef to) { + for (const auto [f, t] : llvm::zip(from, to)) { + forwardQubit(f, t); + } +} + +void QuantumState::canonicalize() { + if (top) { + return; + } + SmallVector negligible; + for (const auto& [key, amp] : amplitudes) { + if (std::abs(amp) <= MATRIX_TOLERANCE) { + negligible.push_back(key); + } + } + for (const uint64_t key : negligible) { + amplitudes.erase(key); + } + if (amplitudes.size() > maxNonzeroAmplitudes) { + markTop(); + } +} + +LogicalResult QuantumState::applyMatrix1Q(Value in, Value out, + const Matrix2x2& matrix, + ArrayRef ctrlsIn, + ArrayRef ctrlsOut) { + const auto idx = indexOf(in); + if (!idx || ctrlsOut.size() != ctrlsIn.size()) { + return failure(); + } + for (Value c : ctrlsIn) { + if (!contains(c)) { + return failure(); + } + } + if (top) { + forwardQubit(in, out); + forwardQubits(ctrlsIn, ctrlsOut); + return success(); + } + + const uint64_t targetBit = uint64_t{1} << *idx; + const uint64_t ctrlMask = maskOf(ctrlsIn); + + llvm::DenseMap result; + for (const auto& [key, amp] : amplitudes) { + if ((key & ctrlMask) != ctrlMask) { + result[key] += amp; + continue; + } + // Scatter this input's matrix column across both output rows. + const uint64_t base = key & ~targetBit; + const unsigned col = (key & targetBit) != 0 ? 1U : 0U; + result[base] += matrix.data[col] * amp; + result[base | targetBit] += matrix.data[2 + col] * amp; + } + + amplitudes = std::move(result); + forwardQubit(in, out); + forwardQubits(ctrlsIn, ctrlsOut); + canonicalize(); + + return success(); +} + +LogicalResult QuantumState::applyMatrix2Q(Value in0, Value in1, Value out0, + Value out1, const Matrix4x4& matrix, + ArrayRef ctrlsIn, + ArrayRef ctrlsOut) { + const auto idx0 = indexOf(in0); + const auto idx1 = indexOf(in1); + if (!idx0 || !idx1 || *idx0 == *idx1 || ctrlsOut.size() != ctrlsIn.size()) { + return failure(); + } + for (Value c : ctrlsIn) { + if (!contains(c)) { + return failure(); + } + } + if (top) { + forwardQubit(in0, out0); + forwardQubit(in1, out1); + forwardQubits(ctrlsIn, ctrlsOut); + return success(); + } + + // QCO convention: the first target is the high bit of the local 4-index. + const uint64_t hiBit = uint64_t{1} << *idx0; + const uint64_t loBit = uint64_t{1} << *idx1; + const uint64_t bothBits = hiBit | loBit; + const uint64_t ctrlMask = maskOf(ctrlsIn); + + const auto localKey = [&](uint64_t base, unsigned local) { + return base | ((local & 1U) != 0U ? loBit : 0) | + ((local & 2U) != 0U ? hiBit : 0); + }; + const auto localCol = [&](uint64_t key) { + return ((key & hiBit) != 0 ? 2U : 0U) | ((key & loBit) != 0 ? 1U : 0U); + }; + + llvm::DenseMap result; + for (const auto& [key, amp] : amplitudes) { + if ((key & ctrlMask) != ctrlMask) { + result[key] += amp; + continue; + } + // Scatter this input's matrix column across all four output rows. + const uint64_t base = key & ~bothBits; + const unsigned col = localCol(key); + for (unsigned row = 0; row < 4; ++row) { + result[localKey(base, row)] += matrix(row, col) * amp; + } + } + + amplitudes = std::move(result); + forwardQubit(in0, out0); + forwardQubit(in1, out1); + forwardQubits(ctrlsIn, ctrlsOut); + canonicalize(); + + return success(); +} + +LogicalResult QuantumState::applyControlledPhase(double phase, + ArrayRef ctrlsIn, + ArrayRef ctrlsOut) { + if (ctrlsIn.empty() || ctrlsOut.size() != ctrlsIn.size()) { + return failure(); + } + for (Value c : ctrlsIn) { + if (!contains(c)) { + return failure(); + } + } + if (top) { + forwardQubits(ctrlsIn, ctrlsOut); + return success(); + } + const uint64_t ctrlMask = maskOf(ctrlsIn); + const Complex factor = std::exp(Complex{0.0, phase}); + for (auto& [key, amp] : amplitudes) { + if ((key & ctrlMask) == ctrlMask) { + amp *= factor; + } + } + forwardQubits(ctrlsIn, ctrlsOut); + canonicalize(); + return success(); +} + +FailureOr> QuantumState::measure(Value in, + Value out) { + const auto idx = indexOf(in); + if (!idx) { + return failure(); + } + if (top) { + forwardQubit(in, out); + return SmallVector{}; + } + const uint64_t targetBit = uint64_t{1} << *idx; + + llvm::DenseMap zeroAmps; + llvm::DenseMap oneAmps; + double probZero = 0.0; + double probOne = 0.0; + for (const auto& [key, amp] : amplitudes) { + if ((key & targetBit) == 0) { + zeroAmps[key] = amp; + probZero += std::norm(amp); + } else { + oneAmps[key] = amp; + probOne += std::norm(amp); + } + } + + const auto makeBranch = [&](unsigned bit, double probability, + const llvm::DenseMap& amps) { + auto branch = + std::unique_ptr(new QuantumState(maxNonzeroAmplitudes)); + branch->qubits = qubits; + branch->forwardQubit(in, out); + const double scale = 1.0 / std::sqrt(probability); + for (const auto& [key, amp] : amps) { + branch->amplitudes[key] += amp * scale; + } + branch->canonicalize(); + return MeasurementOutcome{ + .bit = bit, .probability = probability, .state = std::move(branch)}; + }; + + SmallVector outcomes; + if (!zeroAmps.empty()) { + outcomes.push_back(makeBranch(0, probZero, zeroAmps)); + } + if (!oneAmps.empty()) { + outcomes.push_back(makeBranch(1, probOne, oneAmps)); + } + return outcomes; +} + +FailureOr> QuantumState::reset(Value in, + Value out) { + auto outcomes = measure(in, out); + if (failed(outcomes)) { + return failure(); + } + for (auto& outcome : *outcomes) { + if (outcome.bit == 0 || outcome.state == nullptr) { + continue; + } + const auto idx = outcome.state->indexOf(out); + if (!idx) { + return failure(); + } + const uint64_t targetBit = uint64_t{1} << *idx; + llvm::DenseMap flipped; + for (const auto& [key, amp] : outcome.state->amplitudes) { + flipped[key & ~targetBit] += amp; + } + outcome.state->amplitudes = std::move(flipped); + outcome.state->canonicalize(); + } + return outcomes; +} + +QuantumState QuantumState::unify(const QuantumState& that) const { + QuantumState result(maxNonzeroAmplitudes); + result.qubits.append(qubits.begin(), qubits.end()); + result.qubits.append(that.qubits.begin(), that.qubits.end()); + + if (top || that.top || result.qubits.size() > MAX_GROUP_QUBITS || + static_cast(amplitudes.size()) * that.amplitudes.size() > + maxNonzeroAmplitudes) { + result.markTop(); + return result; + } + + const auto shift = qubits.size(); + for (const auto& [keyA, ampA] : amplitudes) { + for (const auto& [keyB, ampB] : that.amplitudes) { + result.amplitudes[keyA | keyB << shift] += ampA * ampB; + } + } + result.canonicalize(); + return result; +} + +bool QuantumState::isAlwaysZero(Value q) const { + const auto idx = indexOf(q); + if (top || !idx || amplitudes.empty()) { + return false; + } + return llvm::all_of(amplitudes, [&](const auto& entry) { + return (entry.first >> *idx & uint64_t{1}) == 0; + }); +} + +bool QuantumState::isAlwaysOne(Value q) const { + const auto idx = indexOf(q); + if (top || !idx || amplitudes.empty()) { + return false; + } + return llvm::all_of(amplitudes, [&](const auto& entry) { + return (entry.first >> *idx & uint64_t{1}) == 1; + }); +} + +bool QuantumState::hasAlwaysZeroAmplitude( + ArrayRef> basis) const { + if (top) { + return false; + } + uint64_t mask = 0; + uint64_t wanted = 0; + for (const auto& [qubit, one] : basis) { + const auto idx = indexOf(qubit); + if (!idx) { + continue; + } + mask |= uint64_t{1} << *idx; + if (one) { + wanted |= uint64_t{1} << *idx; + } + } + return llvm::all_of(amplitudes, [&](const auto& entry) { + return (entry.first & mask) != wanted; + }); +} + +bool QuantumState::operator==(const QuantumState& that) const { + if (top || that.top) { + return top == that.top; + } + if (maxNonzeroAmplitudes != that.maxNonzeroAmplitudes || + qubits.size() != that.qubits.size() || + amplitudes.size() != that.amplitudes.size()) { + return false; + } + if (!std::equal(qubits.begin(), qubits.end(), that.qubits.begin())) { + return false; + } + return llvm::all_of(amplitudes, [&](const auto& entry) { + const auto it = that.amplitudes.find(entry.first); + return it != that.amplitudes.end() && + std::abs(entry.second - it->second) <= MATRIX_TOLERANCE; + }); +} + +void QuantumState::print(raw_ostream& os) const { + if (top) { + os << ""; + return; + } + if (qubits.empty()) { + return; + } + + const std::map ordered(amplitudes.begin(), amplitudes.end()); + bool first = true; + for (const auto& [key, amp] : ordered) { + if (!first) { + os << ", "; + } + first = false; + + os << '|'; + for (size_t bit = qubits.size(); bit-- > 0;) { + os << (((key >> bit) & uint64_t{1}) != 0 ? '1' : '0'); + } + os << "> -> "; + + SmallString<16> buf; + llvm::raw_svector_ostream(buf) << llvm::format("%.2f", amp.real()); + const llvm::StringRef real(buf); + os << (real == "-0.00" ? StringRef("0.00") : real); + + if (std::abs(amp.imag()) > MATRIX_TOLERANCE) { + os << (amp.imag() > 0 ? " + i" : " - i") + << llvm::format("%.2f", std::abs(amp.imag())); + } + } +} + +} // namespace mlir::qco diff --git a/mlir/lib/Dialect/QCO/Transforms/Optimizations/ConstantPropagation/QuantumState.hpp b/mlir/lib/Dialect/QCO/Transforms/Optimizations/ConstantPropagation/QuantumState.hpp new file mode 100644 index 0000000000..1bb214b78f --- /dev/null +++ b/mlir/lib/Dialect/QCO/Transforms/Optimizations/ConstantPropagation/QuantumState.hpp @@ -0,0 +1,255 @@ +/* + * Copyright (c) 2023 - 2026 Chair for Design Automation, TUM + * Copyright (c) 2025 - 2026 Munich Quantum Software Company GmbH + * All rights reserved. + * + * SPDX-License-Identifier: MIT + * + * Licensed under the MIT License + */ + +#pragma once + +#include "mlir/Dialect/QCO/Utils/Matrix.h" + +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +namespace mlir::qco { + +class QuantumState; + +/** + * @brief One branch of a measurement or reset: the observed bit, its + * probability, and the normalized post-measurement state. + */ +struct MeasurementOutcome { + unsigned bit; + double probability; + std::unique_ptr state; +}; + +/** + * @brief One entanglement group: a pure quantum state over a set of qubit SSA + * values. + * + * The state is a map from computational-basis index to complex amplitude. The + * bit at position i of a basis index refers to the qubit at position i of the + * managed vector. + * + * If the number of non-zero amplitudes exceeds the threshold + * maxNonzeroAmplitudes, or the group holds more than 63 qubits (the basis + * index is a uint64_t), the state collapses to top. + */ +class QuantumState { + bool top = false; + size_t maxNonzeroAmplitudes; + SmallVector qubits; + llvm::DenseMap amplitudes; + + explicit QuantumState(size_t maxNonzeroAmplitudes) + : maxNonzeroAmplitudes(maxNonzeroAmplitudes) {} + + /// @brief Bitmask of the positions of the given values that are in the group. + [[nodiscard("QuantumState::maskOf called but ignored")]] uint64_t + maskOf(ArrayRef values) const; + + /// @brief Drops negligible amplitudes and collapses to top if the conditions + /// are met. + void canonicalize(); + +public: + /** + * @brief Builds the all-zero state |0...0> over the qubits. + * + * @param qubits The qubit values of the group, in bit-position order. + * @param maxNonzeroAmplitudes Amplitude budget before the state becomes top. + */ + QuantumState(ArrayRef qubits, size_t maxNonzeroAmplitudes); + + /// @brief Builds a QuantumState for a single-qubit in state |0>. + static QuantumState singletonZero(Value qubit, size_t maxNonzeroAmplitudes); + + [[nodiscard("QuantumState::isTop called but ignored")]] bool isTop() const { + return top; + } + [[nodiscard("QuantumState::getQubits called but ignored")]] ArrayRef + getQubits() const { + return qubits; + } + + /// @brief Whether QuantumState contains the qubit. + [[nodiscard("QuantumState::contains called but ignored")]] bool + contains(Value q) const { + return indexOf(q).has_value(); + } + /// @brief The bit position of a qubit, if present. + [[nodiscard( + "QuantumState::indexOf called but ignored")]] std::optional + indexOf(Value q) const; + + /// @brief Collapses the state to top. + void markTop(); + + /// @brief Changes qubit from to qubit to in place. No-op if QuantumState does + /// not contain from. + void forwardQubit(Value from, Value to); + + /// @brief forwardQubit for each from[i] -> to[i]. to is empty (no rename) or + /// the same length as from. + void forwardQubits(ArrayRef from, ArrayRef to); + + /** + * @brief Applies a single-qubit unitary to qubit in, renaming it to qubit + * out. + * + * When ctrls is non-empty the matrix is applied only on the subspace where + * every control qubit is |1>; the rest of the state passes through. Does + * nothing but the rename when the state is top. + * + * @param in The qubit to apply the matrix to. + * @param out The qubit that in is changed to. + * @param matrix The matrix to apply to the amplitudes of in. + * @param ctrlsIn The qubits that have to be |1> to apply the matrix. + * @param ctrlsOut The controls' post-gate names (empty = unchanged, else same + * length as ctrlsIn). + * @return failure() if in or a control is not in this group (a + * caller/propagation bug - the interpreter must co-locate a gate's targets + * and controls before applying it), or the control in/out lengths mismatch; + * success() otherwise. + */ + [[nodiscard("QuantumState::applyMatrix1Q called but ignored")]] LogicalResult + applyMatrix1Q(Value in, Value out, const Matrix2x2& matrix, + ArrayRef ctrlsIn = {}, ArrayRef ctrlsOut = {}); + + /** + * @brief Applies a two-qubit unitary to in0 and in1, renaming them to out0, + * out1. + * + * Qubit ordering follows QCO's @ref Matrix4x4 convention: in0 is the high bit + * of the 4-dimensional local index, in1 the low bit. When ctrls is non-empty + * the matrix is applied only on the subspace where every control qubit is + * |1>; the rest of the state passes through. Does nothing but the renames + * when the state is top. + * + * @param in0 The high bit the matrx is applied to. + * @param in1 The low bit the matrx is applied to. + * @param out0 The qubit that in0 is changed to. + * @param out1 The qubit that in1 is changed to. + * @param matrix The matrix to apply to the amplitudes of in0 and in1. + * @param ctrlsIn The qubits that have to be |1> to apply the matrix. + * @param ctrlsOut The controls' post-gate names (empty = unchanged, else same + * length as ctrlsIn). + * @return failure() if in0, in1, or a control is not in this group, in0 and + * in1 are the same bit position, or the control in/out lengths mismatch (a + * caller/propagation bug); success() otherwise. + */ + [[nodiscard("QuantumState::applyMatrix2Q called but ignored")]] LogicalResult + applyMatrix2Q(Value in0, Value in1, Value out0, Value out1, + const Matrix4x4& matrix, ArrayRef ctrlsIn = {}, + ArrayRef ctrlsOut = {}); + + /** + * @brief Multiplies by exp(i*phase) the amplitudes where every control is + * |1>. + * + * This function applies a relative phase when there is a controlled global + * phase. An uncontrolled (global) phase is physically unobservable and not + * recoverable from an amplitude map, so it is tracked by HybridState instead + * and rejected here. + * + * @param phase The phase to apply. + * @param ctrlsIn The qubits that all have to be |1> for the phase to apply. + * @param ctrlsOut The controls' post-gate names (empty = unchanged, else same + * length as ctrlsIn). + * @return failure() if ctrlsIn is empty, a control is not in this group, or + * the control in/out lengths mismatch. + */ + [[nodiscard("QuantumState::applyControlledPhase called but ignored")]] + LogicalResult applyControlledPhase(double phase, ArrayRef ctrlsIn, + ArrayRef ctrlsOut = {}); + + /** + * @brief Projective measurement of qubit in in the computational basis. + * + * Each branch's state is re-normalized and holds the measured qubit (now + * definite) under its post-measurement name out. + * + * @param in The qubit that is being measured. + * @param out The measured qubit's post-measurement name. + * @return failure() if in is not in the group (a caller/propagation bug). + * Otherwise: an empty list if the state is top, one branch if the outcome is + * deterministic, two branches otherwise. + */ + [[nodiscard("QuantumState::measure called but ignored")]] + FailureOr> measure(Value in, Value out); + + /** + * @brief Reset of qubit in: measure, then force the qubit to |0>. + * + * Each branch's state is re-normalized and holds the reset qubit (now |0>) + * under its post-reset name out. + * + * @param in The qubit that is being reset. + * @param out The reset qubit's post-reset name. + * @return failure() if in is not in the group (a caller/propagation bug). + * Otherwise: an empty list if the state is top, one branch if the outcome is + * deterministic, two branches otherwise. + */ + [[nodiscard("QuantumState::reset called but ignored")]] + FailureOr> reset(Value in, Value out); + + /** + * @brief Tensor product of this group with that. + * + * The result's qubits are this->getQubits() followed by that.getQubits(). + * Becomes top if either operand is top or the product exceeds this group's + * maximally allowed amplitude number. The two groups must not share qubits. + * + * @param that The QuantumState to unify this with. + */ + [[nodiscard("QuantumState::unify called but ignored")]] QuantumState + unify(const QuantumState& that) const; + + /// @brief Whether every non-zero amplitude has q set to zero. + [[nodiscard("QuantumState::isAlwaysZero called but ignored")]] bool + isAlwaysZero(Value q) const; + + /// @brief Whether every non-zero amplitude has q set to one. + [[nodiscard("QuantumState::isAlwaysOne called but ignored")]] bool + isAlwaysOne(Value q) const; + + /** + * @brief Whether the given qubit basis never occurs. + * + * @param basis Pairs of (qubit, expected bit value); qubits not in the + * group are ignored. Returns true when no non-zero amplitude matches all + * the (in-group) pairs simultaneously. + */ + [[nodiscard("QuantumState::hasAlwaysZeroAmplitude called but ignored")]] bool + hasAlwaysZeroAmplitude(ArrayRef> basis) const; + + [[nodiscard("QuantumState::== called but ignored")]] bool + operator==(const QuantumState& that) const; + + /** + * @brief Human-readable dump, e.g. "|010> -> 0.71, |110> -> -0.71". + * + * Used by the enclosing HybridState / UnionTable / lattice print overrides + * and for debugging. Basis states are listed in ascending index order; bit i + * of the printed string (from the right) is getQubits()[i]. Amplitudes use + * two decimals and an "+ i" / "- i" imaginary part when non-negligible. + * Prints nothing for a group with no qubits. + */ + void print(raw_ostream& os) const; +}; + +} // namespace mlir::qco diff --git a/mlir/lib/Dialect/QCO/Transforms/Optimizations/ConstantPropagation/Rewriter.cpp b/mlir/lib/Dialect/QCO/Transforms/Optimizations/ConstantPropagation/Rewriter.cpp new file mode 100644 index 0000000000..1655360835 --- /dev/null +++ b/mlir/lib/Dialect/QCO/Transforms/Optimizations/ConstantPropagation/Rewriter.cpp @@ -0,0 +1,160 @@ +/* + * Copyright (c) 2023 - 2026 Chair for Design Automation, TUM + * Copyright (c) 2025 - 2026 Munich Quantum Software Company GmbH + * All rights reserved. + * + * SPDX-License-Identifier: MIT + * + * Licensed under the MIT License + */ + +#include "Rewriter.hpp" + +#include "ConstantPropagationAnalysis.hpp" +#include "Decisions.hpp" +#include "UnionTable.hpp" +#include "mlir/Dialect/QCO/IR/QCOOps.h" + +#include +#include +#include +#include +#include +#include + +namespace mlir::qco { + +SmallVector collectDecisions(func::FuncOp entry, + DataFlowSolver& solver) { + SmallVector decisions; + + entry.walk([&](CtrlOp op) { + // A controlled gate nested in another modifier's body is interpreted by + // that modifier's handler and not rewritten. + if (op->getParentOfType() || op->getParentOfType() || + op->getParentOfType()) { + return; + } + + const auto* lattice = + solver.lookupState(solver.getProgramPointBefore(op)); + if (lattice == nullptr || !lattice->isInitialized()) { + return; + } + const UnionTable& table = lattice->getUnionTable(); + if (table.isAllTop()) { + return; + } + + const SmallVector controls(op.getInputControls().begin(), + op.getInputControls().end()); + + if (!table.areControlsSatisfiable(controls)) { + decisions.push_back(DropOp{op}); + return; + } + + const SuperfluousResult superfluous = + table.getSuperfluousControls(controls); + if (superfluous.completelySuperfluous) { + decisions.push_back(DropOp{op}); + return; + } + + SmallVector dropIndices; + for (const auto& [index, control] : llvm::enumerate(controls)) { + if (superfluous.superfluousQubits.contains(control)) { + dropIndices.push_back(static_cast(index)); + } + } + // A strict subset is stripped; all of them means the gate fires + // unconditionally and its body is inlined (see applyStrip). + if (!dropIndices.empty()) { + decisions.push_back(StripControls{op, std::move(dropIndices)}); + } + }); + + return decisions; +} + +/// @brief Erases a never-firing controlled gate: every output qubit is replaced +/// by the matching input. +static void applyDrop(const DropOp& drop, IRRewriter& rewriter) { + CtrlOp op = drop.op; + for (auto [in, out] : + llvm::zip_equal(op.getInputQubits(), op.getOutputQubits())) { + rewriter.replaceAllUsesWith(out, in); + } + rewriter.eraseOp(op); +} + +/// @brief Removes always-satisfied controls from a controlled gate. +/// +/// A CtrlOp's body block arguments alias its *targets* only - controls merely +/// pass through - so dropping a subset just rebuilds the op around the same +/// body. Dropping every control means the body runs unconditionally: it is +/// inlined in place of the op, with the target block arguments bound to the +/// target operands and the yielded values taking over the op's target results. +static void applyStrip(const StripControls& strip, IRRewriter& rewriter) { + CtrlOp op = strip.op; + const auto controlsIn = op.getInputControls(); + const auto isDropped = [&](size_t index) { + return llvm::is_contained(strip.dropControlIndices, + static_cast(index)); + }; + + SmallVector keptControls; + for (const auto& [index, control] : llvm::enumerate(controlsIn)) { + if (!isDropped(index)) { + keptControls.push_back(control); + } + } + + rewriter.setInsertionPoint(op); + + if (keptControls.empty()) { + Block& body = op.getRegion().front(); + auto yield = cast(body.getTerminator()); + const auto yielded = yield.getOperands(); + rewriter.inlineBlockBefore(&body, op, op.getInputTargets()); + for (auto [result, value] : + llvm::zip_equal(op.getOutputTargets(), yielded)) { + rewriter.replaceAllUsesWith(result, value); + } + for (auto [result, control] : + llvm::zip_equal(op.getOutputControls(), controlsIn)) { + rewriter.replaceAllUsesWith(result, control); + } + rewriter.eraseOp(yield); + rewriter.eraseOp(op); + return; + } + + auto newOp = + CtrlOp::create(rewriter, op.getLoc(), keptControls, op.getInputTargets()); + rewriter.inlineRegionBefore(op.getRegion(), newOp.getRegion(), + newOp.getRegion().end()); + + for (const auto& [index, control] : llvm::enumerate(controlsIn)) { + rewriter.replaceAllUsesWith( + op.getOutputControl(index), + isDropped(index) ? control : newOp.getOutputForInput(control)); + } + for (const auto& [index, target] : llvm::enumerate(op.getInputTargets())) { + rewriter.replaceAllUsesWith(op.getOutputTarget(index), + newOp.getOutputForInput(target)); + } + rewriter.eraseOp(op); +} + +void applyDecisions(ArrayRef decisions, IRRewriter& rewriter) { + for (const Decision& decision : decisions) { + if (const auto* drop = std::get_if(&decision)) { + applyDrop(*drop, rewriter); + } else { + applyStrip(std::get(decision), rewriter); + } + } +} + +} // namespace mlir::qco diff --git a/mlir/lib/Dialect/QCO/Transforms/Optimizations/ConstantPropagation/Rewriter.hpp b/mlir/lib/Dialect/QCO/Transforms/Optimizations/ConstantPropagation/Rewriter.hpp new file mode 100644 index 0000000000..f830cf83fc --- /dev/null +++ b/mlir/lib/Dialect/QCO/Transforms/Optimizations/ConstantPropagation/Rewriter.hpp @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2023 - 2026 Chair for Design Automation, TUM + * Copyright (c) 2025 - 2026 Munich Quantum Software Company GmbH + * All rights reserved. + * + * SPDX-License-Identifier: MIT + * + * Licensed under the MIT License + */ + +#pragma once + +#include "Decisions.hpp" + +#include +#include +#include +#include + +namespace mlir::qco { + +/** + * @brief Walks entry in program order and, from the constant-propagation facts + * already computed in solver, collects the rewrites to perform. + * + * Pure: touches no IR. Only top-level controlled gates are considered - a + * CtrlOp nested in another modifier body is left alone. A program point with no + * lattice, an uninitialised lattice, or an all-top table yields no decision for + * that op. + */ +[[nodiscard]] SmallVector collectDecisions(func::FuncOp entry, + DataFlowSolver& solver); + +/** + * @brief Applies decisions to the IR via rewriter. + * + * Decisions are independent (distinct ops, no nested-body overlap) and use + * operand indices rather than values, so batch application is + * order-insensitive. + */ +void applyDecisions(ArrayRef decisions, IRRewriter& rewriter); + +} // namespace mlir::qco diff --git a/mlir/lib/Dialect/QCO/Transforms/Optimizations/ConstantPropagation/UnionTable.cpp b/mlir/lib/Dialect/QCO/Transforms/Optimizations/ConstantPropagation/UnionTable.cpp new file mode 100644 index 0000000000..68328aaece --- /dev/null +++ b/mlir/lib/Dialect/QCO/Transforms/Optimizations/ConstantPropagation/UnionTable.cpp @@ -0,0 +1,718 @@ +/* + * Copyright (c) 2023 - 2026 Chair for Design Automation, TUM + * Copyright (c) 2025 - 2026 Munich Quantum Software Company GmbH + * All rights reserved. + * + * SPDX-License-Identifier: MIT + * + * Licensed under the MIT License + */ + +#include "UnionTable.hpp" + +#include "HybridState.hpp" +#include "QuantumState.hpp" +#include "mlir/Dialect/QCO/Utils/Matrix.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +namespace mlir::qco { + +/// The qubit set of a slot, order-normalized, so two slots (from sibling +/// control-flow paths) can be matched. +static std::vector qubitKey(const UnionTable::Slot& slot) { + std::vector key; + key.reserve(slot.front().getQubits().size()); + for (Value q : slot.front().getQubits()) { + key.push_back(q.getAsOpaquePointer()); + } + llvm::sort(key); + return key; +} + +//===----------------------------------------------------------------------===// +// Partition helpers +//===----------------------------------------------------------------------===// + +std::optional UnionTable::slotIndexContaining(Value v) const { + for (const auto& [i, slot] : llvm::enumerate(slots)) { + if (slot.front().hasQubit(v)) { + return static_cast(i); + } + for (const auto& hs : slot) { + if (hs.getClassical(v).has_value()) { + return static_cast(i); + } + } + } + return std::nullopt; +} + +SmallVector UnionTable::slotsTouchedBy(ArrayRef values) const { + SmallVector result; + for (Value v : values) { + if (const auto i = slotIndexContaining(v)) { + if (!llvm::is_contained(result, *i)) { + result.push_back(*i); + } + } + } + llvm::sort(result); + return result; +} + +HybridState UnionTable::reducedRepresentative(const Slot& slot) { + HybridState representative = slot.front(); + for (size_t j = 1; j < slot.size(); ++j) { + representative.intersectClassical(slot[j]); + } + return representative; +} + +void UnionTable::mergeSlots(ArrayRef values) { + if (allTop) { + return; + } + const auto touched = slotsTouchedBy(values); + if (touched.size() <= 1) { + return; + } + + size_t product = 1; + bool overflow = false; + for (const unsigned i : touched) { + if (product > maxHybridStates / slots[i].size()) { + overflow = true; + break; + } + product *= slots[i].size(); + } + + Slot fused; + if (overflow) { + const auto toppedRepresentative = [](const Slot& slot) { + HybridState representative = reducedRepresentative(slot); + representative.markStateTop(); + return representative; + }; + HybridState top = toppedRepresentative(slots[touched.front()]); + for (size_t k = 1; k < touched.size(); ++k) { + top = top.tensor(toppedRepresentative(slots[touched[k]])); + } + top.setProbability(1.0); + fused.push_back(std::move(top)); + } else { + fused = slots[touched.front()]; + for (size_t k = 1; k < touched.size(); ++k) { + const Slot& next = slots[touched[k]]; + Slot combined; + combined.reserve(fused.size() * next.size()); + for (const auto& a : fused) { + for (const auto& b : next) { + combined.push_back(a.tensor(b)); + } + } + fused = std::move(combined); + } + } + + // Erase the merged slots high-index-first, then append the fused one. + for (size_t k = touched.size(); k-- > 0;) { + slots.erase(slots.begin() + touched[k]); + } + slots.push_back(std::move(fused)); +} + +UnionTable::Slot UnionTable::mergeAlternatives(const Slot& a, const Slot& b) { + Slot merged; + const auto absorb = [&merged](const Slot& side) { + for (const auto& hs : side) { + HybridState* match = nullptr; + for (auto& candidate : merged) { + if (candidate.sameConfiguration(hs)) { + match = &candidate; + break; + } + } + if (match != nullptr) { + match->setProbability(match->getProbability() + hs.getProbability()); + } else { + merged.push_back(hs); + } + } + }; + absorb(a); + absorb(b); + + double sum = 0.0; + for (const auto& hs : merged) { + sum += hs.getProbability(); + } + if (sum > MATRIX_TOLERANCE) { + for (auto& hs : merged) { + hs.scaleProbability(1.0 / sum); + } + } + return merged; +} + +bool UnionTable::sameSlot(const Slot& a, const Slot& b) { + if (a.size() != b.size()) { + return false; + } + SmallVector used(b.size(), false); + for (const auto& lhs : a) { + bool matched = false; + for (unsigned j = 0; j < b.size(); ++j) { + if (!used[j] && lhs == b[j]) { + used[j] = true; + matched = true; + break; + } + } + if (!matched) { + return false; + } + } + return true; +} + +//===----------------------------------------------------------------------===// +// Seeding +//===----------------------------------------------------------------------===// + +void UnionTable::seedQubit(Value qubit) { + if (allTop || isTracked(qubit)) { + return; + } + Slot slot; + slot.emplace_back(QuantumState::singletonZero(qubit, maxNonzeroAmplitudes), + maxNonzeroAmplitudes, 1.0); + slots.push_back(std::move(slot)); +} + +void UnionTable::seedClassical(Value value, Attribute attr) { + if (allTop) { + return; + } + if (const auto i = slotIndexContaining(value)) { + for (auto& hs : slots[*i]) { + hs.setClassical(value, attr); + } + return; + } + Slot slot; + slot.emplace_back(QuantumState(ArrayRef{}, maxNonzeroAmplitudes), + maxNonzeroAmplitudes, 1.0); + slot.back().setClassical(value, attr); + slots.push_back(std::move(slot)); +} + +bool UnionTable::isTracked(Value v) const { + return slotIndexContaining(v).has_value(); +} + +//===----------------------------------------------------------------------===// +// SSA forwarding +//===----------------------------------------------------------------------===// + +void UnionTable::forwardValue(Value from, Value to) { + for (auto& slot : slots) { + for (auto& hs : slot) { + hs.forwardValue(from, to); + } + } +} + +void UnionTable::forwardValues(ArrayRef from, ArrayRef to) { + for (const auto [f, t] : llvm::zip(from, to)) { + forwardValue(f, t); + } +} + +//===----------------------------------------------------------------------===// +// Operation propagation +//===----------------------------------------------------------------------===// + +LogicalResult UnionTable::applyMatrix1Q(Value in, Value out, + const Matrix2x2& matrix, + ArrayRef quantumCtrlsIn, + ArrayRef quantumCtrlsOut, + ArrayRef posClassicalCtrls, + ArrayRef negClassicalCtrls) { + if (allTop) { + return success(); + } + if (!isTracked(in)) { + return failure(); + } + + SmallVector touched{in}; + touched.append(quantumCtrlsIn.begin(), quantumCtrlsIn.end()); + touched.append(posClassicalCtrls.begin(), posClassicalCtrls.end()); + touched.append(negClassicalCtrls.begin(), negClassicalCtrls.end()); + mergeSlots(touched); + if (allTop) { + return success(); + } + + for (auto& hs : slots[*slotIndexContaining(in)]) { + if (failed(hs.applyMatrix1Q(in, out, matrix, quantumCtrlsIn, + quantumCtrlsOut, posClassicalCtrls, + negClassicalCtrls))) { + return failure(); + } + } + return success(); +} + +LogicalResult UnionTable::applyMatrix2Q(Value in0, Value in1, Value out0, + Value out1, const Matrix4x4& matrix, + ArrayRef quantumCtrlsIn, + ArrayRef quantumCtrlsOut, + ArrayRef posClassicalCtrls, + ArrayRef negClassicalCtrls) { + if (allTop) { + return success(); + } + if (!isTracked(in0) || !isTracked(in1)) { + return failure(); + } + + SmallVector touched{in0, in1}; + touched.append(quantumCtrlsIn.begin(), quantumCtrlsIn.end()); + touched.append(posClassicalCtrls.begin(), posClassicalCtrls.end()); + touched.append(negClassicalCtrls.begin(), negClassicalCtrls.end()); + mergeSlots(touched); + if (allTop) { + return success(); + } + + for (auto& hs : slots[*slotIndexContaining(in0)]) { + if (failed(hs.applyMatrix2Q(in0, in1, out0, out1, matrix, quantumCtrlsIn, + quantumCtrlsOut, posClassicalCtrls, + negClassicalCtrls))) { + return failure(); + } + } + return success(); +} + +LogicalResult UnionTable::addGlobalPhase(Value theta, + ArrayRef quantumCtrlsIn, + ArrayRef quantumCtrlsOut, + ArrayRef posClassicalCtrls, + ArrayRef negClassicalCtrls) { + if (allTop) { + return success(); + } + if ((quantumCtrlsIn.empty() && !quantumCtrlsOut.empty()) || + !isTracked(theta)) { + return failure(); + } + + SmallVector touched{theta}; + touched.append(quantumCtrlsIn.begin(), quantumCtrlsIn.end()); + touched.append(posClassicalCtrls.begin(), posClassicalCtrls.end()); + touched.append(negClassicalCtrls.begin(), negClassicalCtrls.end()); + mergeSlots(touched); + if (allTop) { + return success(); + } + + Value anchor = theta; + if (!quantumCtrlsIn.empty()) { + anchor = quantumCtrlsIn.front(); + } else if (!posClassicalCtrls.empty()) { + anchor = posClassicalCtrls.front(); + } else if (!negClassicalCtrls.empty()) { + anchor = negClassicalCtrls.front(); + } + + const auto slot = slotIndexContaining(anchor); + if (!slot) { + return failure(); + } + for (auto& hs : slots[*slot]) { + if (failed(hs.addGlobalPhase(theta, quantumCtrlsIn, quantumCtrlsOut, + posClassicalCtrls, negClassicalCtrls))) { + return failure(); + } + } + return success(); +} + +void UnionTable::propagateClassical(Operation* op) { + if (allTop) { + return; + } + const SmallVector operands(op->getOperands().begin(), + op->getOperands().end()); + mergeSlots(operands); + if (allTop) { + return; + } + for (Value operand : operands) { + if (const auto slot = slotIndexContaining(operand)) { + for (auto& hs : slots[*slot]) { + hs.propagateClassical(op); + } + return; + } + } +} + +LogicalResult UnionTable::measureQubit(Value in, Value out, + Value classicalResult, + ArrayRef posClassicalCtrls, + ArrayRef negClassicalCtrls) { + if (allTop) { + return success(); + } + if (!isTracked(in)) { + return failure(); + } + + SmallVector touched{in}; + touched.push_back(classicalResult); + touched.append(posClassicalCtrls.begin(), posClassicalCtrls.end()); + touched.append(negClassicalCtrls.begin(), negClassicalCtrls.end()); + mergeSlots(touched); + if (allTop) { + return success(); + } + + for (auto& hs : slots[*slotIndexContaining(in)]) { + if (failed(hs.measureQubit(in, out, classicalResult, posClassicalCtrls, + negClassicalCtrls))) { + return failure(); + } + } + return success(); +} + +LogicalResult UnionTable::resetQubit(Value in, Value out, + ArrayRef posClassicalCtrls, + ArrayRef negClassicalCtrls) { + if (allTop) { + return success(); + } + if (!isTracked(in)) { + return failure(); + } + + SmallVector touched{in}; + touched.append(posClassicalCtrls.begin(), posClassicalCtrls.end()); + touched.append(negClassicalCtrls.begin(), negClassicalCtrls.end()); + mergeSlots(touched); + if (allTop) { + return success(); + } + + for (auto& hs : slots[*slotIndexContaining(in)]) { + if (failed(hs.resetQubit(in, out, posClassicalCtrls, negClassicalCtrls))) { + return failure(); + } + } + return success(); +} + +void UnionTable::markQubitsTop(ArrayRef qubits) { + if (allTop) { + return; + } + llvm::DenseSet done; + for (Value q : qubits) { + const auto i = slotIndexContaining(q); + if (i && done.insert(*i).second) { + for (auto& hs : slots[*i]) { + hs.markStateTop(); + } + } + } +} + +//===----------------------------------------------------------------------===// +// Queries +//===----------------------------------------------------------------------===// + +bool UnionTable::isQubitAlwaysOne(Value q) const { + if (allTop) { + return false; + } + const auto i = slotIndexContaining(q); + return i && llvm::all_of(slots[*i], [&](const HybridState& hs) { + return hs.isQubitAlwaysOne(q); + }); +} + +bool UnionTable::isQubitAlwaysZero(Value q) const { + if (allTop) { + return false; + } + const auto i = slotIndexContaining(q); + return i && llvm::all_of(slots[*i], [&](const HybridState& hs) { + return hs.isQubitAlwaysZero(q); + }); +} + +bool UnionTable::isClassicalAlwaysTrue(Value v) const { + if (allTop) { + return false; + } + const auto i = slotIndexContaining(v); + return i && llvm::all_of(slots[*i], [&](const HybridState& hs) { + return hs.isClassicalTrue(v); + }); +} + +bool UnionTable::isClassicalAlwaysFalse(Value v) const { + if (allTop) { + return false; + } + const auto i = slotIndexContaining(v); + return i && llvm::all_of(slots[*i], [&](const HybridState& hs) { + return hs.isClassicalFalse(v); + }); +} + +bool UnionTable::areControlsSatisfiable( + ArrayRef quantumCtrls, ArrayRef posClassicalCtrls, + ArrayRef negClassicalCtrls) const { + if (allTop) { + return true; + } + + SmallVector all(quantumCtrls.begin(), quantumCtrls.end()); + all.append(posClassicalCtrls.begin(), posClassicalCtrls.end()); + all.append(negClassicalCtrls.begin(), negClassicalCtrls.end()); + + for (const unsigned si : slotsTouchedBy(all)) { + const Slot& slot = slots[si]; + const auto hasClassical = [&](Value c) { + return llvm::any_of(slot, [&](const HybridState& hs) { + return hs.getClassical(c).has_value(); + }); + }; + + SmallVector quantum; + for (Value c : quantumCtrls) { + if (slot.front().hasQubit(c)) { + quantum.push_back(c); + } + } + SmallVector pos; + for (Value c : posClassicalCtrls) { + if (hasClassical(c)) { + pos.push_back(c); + } + } + SmallVector neg; + for (Value c : negClassicalCtrls) { + if (hasClassical(c)) { + neg.push_back(c); + } + } + if (quantum.empty() && pos.empty() && neg.empty()) { + continue; + } + const bool anySatisfiable = llvm::any_of(slot, [&](const HybridState& hs) { + return hs.areControlsSatisfiable(quantum, pos, neg); + }); + if (!anySatisfiable) { + return false; + } + } + return true; +} + +SuperfluousResult +UnionTable::getSuperfluousControls(ArrayRef quantumCtrls, + ArrayRef posClassicalCtrls, + ArrayRef negClassicalCtrls) const { + SuperfluousResult result; + if (!areControlsSatisfiable(quantumCtrls, posClassicalCtrls, + negClassicalCtrls)) { + result.completelySuperfluous = true; + return result; + } + for (Value q : quantumCtrls) { + if (isQubitAlwaysOne(q)) { + result.superfluousQubits.insert(q); + } + } + for (Value p : posClassicalCtrls) { + if (isClassicalAlwaysTrue(p)) { + result.superfluousClassicalValues.insert(p); + } + } + for (Value n : negClassicalCtrls) { + if (isClassicalAlwaysFalse(n)) { + result.superfluousClassicalValues.insert(n); + } + } + return result; +} + +//===----------------------------------------------------------------------===// +// Lattice support +//===----------------------------------------------------------------------===// + +void UnionTable::join(const UnionTable& other) { + if (allTop || other.allTop) { + markAllTop(); + return; + } + + SmallVector myQuantum; + SmallVector myClassical; + for (const auto& [i, slot] : llvm::enumerate(slots)) { + (slot.front().getQubits().empty() ? myClassical : myQuantum) + .push_back(static_cast(i)); + } + SmallVector theirQuantum; + SmallVector theirClassical; + for (const auto& [i, slot] : llvm::enumerate(other.slots)) { + (slot.front().getQubits().empty() ? theirClassical : theirQuantum) + .push_back(static_cast(i)); + } + + if (myQuantum.size() != theirQuantum.size()) { + markAllTop(); // different entanglement structure + return; + } + + SmallVector merged; + + for (const unsigned mi : myQuantum) { + const auto key = qubitKey(slots[mi]); + const Slot* theirs = nullptr; + for (const unsigned ti : theirQuantum) { + if (qubitKey(other.slots[ti]) == key) { + theirs = &other.slots[ti]; + break; + } + } + if (theirs == nullptr) { + markAllTop(); + return; + } + + Slot combined = mergeAlternatives(slots[mi], *theirs); + if (combined.size() > maxHybridStates) { + // Too many alternatives for this factor: collapse just this slot to top. + HybridState top = reducedRepresentative(combined); + top.markStateTop(); + top.setProbability(1.0); + combined.clear(); + combined.push_back(std::move(top)); + } + merged.push_back(std::move(combined)); + } + + // A purely classical fact survives only if the other branch asserts the same + // one; otherwise it becomes unknown (it is simply dropped). + for (const unsigned mi : myClassical) { + const bool inBoth = llvm::any_of(theirClassical, [&](unsigned ti) { + return llvm::any_of(other.slots[ti], [&](const HybridState& theirHs) { + return slots[mi].front().sameConfiguration(theirHs); + }); + }); + if (inBoth) { + Slot slot; + slot.push_back(slots[mi].front()); + slot.back().setProbability(1.0); + merged.push_back(std::move(slot)); + } + } + + slots = std::move(merged); +} + +void UnionTable::markAllTop() { + allTop = true; + slots.clear(); +} + +bool UnionTable::areStatesAllTop() const { + if (allTop) { + return true; + } + bool sawQuantum = false; + for (const auto& slot : slots) { + if (slot.front().getQubits().empty()) { + continue; + } + sawQuantum = true; + for (const auto& hs : slot) { + if (!hs.isTop()) { + return false; + } + } + } + return sawQuantum; +} + +bool UnionTable::operator==(const UnionTable& other) const { + if (allTop || other.allTop) { + return allTop == other.allTop; + } + if (slots.size() != other.slots.size()) { + return false; + } + SmallVector used(other.slots.size(), false); + for (const auto& mine : slots) { + bool matched = false; + for (unsigned j = 0; j < other.slots.size(); ++j) { + if (!used[j] && sameSlot(mine, other.slots[j])) { + used[j] = true; + matched = true; + break; + } + } + if (!matched) { + return false; + } + } + return true; +} + +void UnionTable::print(raw_ostream& os) const { + if (allTop) { + os << ""; + return; + } + if (slots.empty()) { + os << ""; + return; + } + bool firstSlot = true; + for (const auto& slot : slots) { + if (!firstSlot) { + os << "\n---\n"; + } + firstSlot = false; + bool firstAlt = true; + for (const auto& hs : slot) { + if (!firstAlt) { + os << "\n"; + } + firstAlt = false; + hs.print(os); + } + } +} + +} // namespace mlir::qco diff --git a/mlir/lib/Dialect/QCO/Transforms/Optimizations/ConstantPropagation/UnionTable.hpp b/mlir/lib/Dialect/QCO/Transforms/Optimizations/ConstantPropagation/UnionTable.hpp new file mode 100644 index 0000000000..e107c2f92a --- /dev/null +++ b/mlir/lib/Dialect/QCO/Transforms/Optimizations/ConstantPropagation/UnionTable.hpp @@ -0,0 +1,403 @@ +/* + * Copyright (c) 2023 - 2026 Chair for Design Automation, TUM + * Copyright (c) 2025 - 2026 Munich Quantum Software Company GmbH + * All rights reserved. + * + * SPDX-License-Identifier: MIT + * + * Licensed under the MIT License + */ + +#pragma once + +#include "HybridState.hpp" +#include "QuantumState.hpp" + +#include +#include +#include +#include +#include +#include + +#include + +namespace mlir::qco { + +/** + * @brief Which controls a controlled operation could drop without changing its + * effect on the current state. + * + * completelySuperfluous means the operation can never fire (some control is + * provably unsatisfiable), so the whole operation is dead. Otherwise, the two + * sets list the individual controls that always hold and can be stripped. + */ +struct SuperfluousResult { + bool completelySuperfluous = false; + llvm::DenseSet superfluousQubits; + llvm::DenseSet superfluousClassicalValues; +}; + +/** + * @brief The abstract state of a whole program point: a probability + * distribution over correlated subsystems. + * + * A UnionTable is a list of *slots*. Each slot is a non-empty list of + * @ref HybridState "HybridStates": + * - Slots are unentangled **tensor factors**; the full state is their product. + * - The HybridStates within a slot are **alternatives** of one probabilistic + * disjunction; they share a qubit set and their probabilities sum to one. + * + * Qubit sets of different slots are disjoint. A slot with no qubits is a + * purely classical factor. + * + * Operations take matrix-level arguments (no Operation*); the analysis maps + * gates to matrices and target/output SSA values. Before a multi-qubit or + * controlled operation the touched slots are merged into one (alternatives + * multiply out via HybridState::tensor); if that exceeds maxHybridStates the + * merged slots collapse to a single top state. A target or control value absent + * from the table is a caller/propagation bug and yields failure(); the analysis + * seeds every qubit before first use. + */ +class UnionTable { +public: + using Slot = SmallVector; + +private: + bool allTop = false; + size_t maxNonzeroAmplitudes = 0; + size_t maxHybridStates = 0; + SmallVector slots; + + /// @brief Index of the slot that holds v (as a qubit or a classical key). + [[nodiscard("UnionTable::slotIndexContaining called but ignored")]] + std::optional slotIndexContaining(Value v) const; + + /// @brief The distinct slot indices touched by any of values, ascending. + [[nodiscard("UnionTable::slotsTouchedBy called but ignored")]] + SmallVector slotsTouchedBy(ArrayRef values) const; + + /** + * @brief Merges every slot touched by values into a single slot. + * + * The merged slot's alternatives are the cartesian product of the merged + * slots' alternatives, combined with HybridState::tensor (probabilities and + * global phases multiply). If the product would exceed maxHybridStates only + * the merged slots collapse to a single top state (untouched slots are left + * alone). Values absent from the table are ignored. + * + * @param values The values whose slots should be merged. + */ + void mergeSlots(ArrayRef values); + + /// @brief A single HybridState standing in for a slot's disjunction: its + /// first alternative, keeping only the classical facts every alternative + /// agrees on. + [[nodiscard("UnionTable::reducedRepresentative called but ignored")]] + static HybridState reducedRepresentative(const Slot& slot); + + /** + * Combines the alternatives of two slots coming from sibling control-flow + * paths: matching configurations are de-duplicated, the result is + * renormalized to sum one. + */ + [[nodiscard("UnionTable::mergeAlternatives called but ignored")]] + static Slot mergeAlternatives(const Slot& a, const Slot& b); + + /// @brief Order-independent equality of two slots' alternatives. + [[nodiscard("UnionTable::sameSlot called but ignored")]] + static bool sameSlot(const Slot& a, const Slot& b); + +public: + /** + * @param maxNonzeroAmplitudes Per-QuantumState amplitude budget before it + * collapses to top. + * @param maxHybridStates Per-slot alternative budget before the whole slot + * collapses to allTop. + */ + UnionTable(size_t maxNonzeroAmplitudes, size_t maxHybridStates) + : maxNonzeroAmplitudes(maxNonzeroAmplitudes), + maxHybridStates(maxHybridStates) {} + + /** + * @brief A zero-budget table (every merge overflows to top). The dataflow + * framework needs a default-constructible lattice payload; the analysis + * overwrites it with a budgeted table before any real state flows through. + */ + UnionTable() = default; + + //===--------------------------------------------------------------------===// + // Seeding + //===--------------------------------------------------------------------===// + + /** + * @brief Adds qubit in state |0> as its own factor. No-op if it is already + * tracked or the table is allTop. + * + * @param qubit The qubit to be added. + */ + void seedQubit(Value qubit); + + /** + * @brief Records value as the resolved classical constant attr in its own + * factor (overwrites an existing entry). No-op if the table is allTop. + * + * @param value The value to be saved. + * @param attr The attribute that value should get. + */ + void seedClassical(Value value, Attribute attr); + + /** + * @brief Whether v is tracked as a qubit or a classical value. + * + * @param v The value to be checked for. + * @returns True if v is already tracked. + */ + [[nodiscard("UnionTable::isTracked called but ignored")]] bool + isTracked(Value v) const; + + //===--------------------------------------------------------------------===// + // SSA forwarding + //===--------------------------------------------------------------------===// + + /** + * @brief Renames from to to everywhere (qubit or classical). No-op if from is + * not present. + * + * @param from The value being replaced. + * @param to The value it is replaced with. + */ + void forwardValue(Value from, Value to); + + /** + * @brief forwardValue for each from[i] -> to[i]. + * + * @param from The values being replaced. + * @param to The values from are replaced with. + */ + void forwardValues(ArrayRef from, ArrayRef to); + + //===--------------------------------------------------------------------===// + // Operation propagation + //===--------------------------------------------------------------------===// + + /** + * @brief Applies a single-qubit unitary to in (renamed to out). + * + * @param in The qubit to apply the matrix to. + * @param out The qubit that in is changed to. + * @param matrix The matrix to apply to the amplitudes of in. + * @param quantumCtrlsIn The qubits that have to be |1> to apply the matrix. + * @param quantumCtrlsOut The qubits that quantumCtrlsIn are changed to. + * @param posClassicalCtrls The classical values that have to be true + * (nonzero) to apply the matrix. + * @param negClassicalCtrls The classical values that have to be false (zero) + * to apply the matrix. + * @return failure() if a target/control value is absent, the control in/out + * lengths mismatch, or a classical control is unresolved. + */ + [[nodiscard("UnionTable::applyMatrix1Q called but ignored")]] LogicalResult + applyMatrix1Q(Value in, Value out, const Matrix2x2& matrix, + ArrayRef quantumCtrlsIn = {}, + ArrayRef quantumCtrlsOut = {}, + ArrayRef posClassicalCtrls = {}, + ArrayRef negClassicalCtrls = {}); + + /** + * @brief Applies a two-qubit unitary to in0, in1 (renamed to out0, out1), + * following QCO's Matrix4x4 convention (in0 = high bit). + * + * @param in0 The high qubit to apply the matrix to. + * @param in1 The low qubit to apply the matrix to. + * @param out0 The qubit that in0 is changed to. + * @param out1 The qubit that in1 is changed to. + * @param matrix The matrix to apply to the amplitudes of in. + * @param quantumCtrlsIn The qubits that have to be |1> to apply the matrix. + * @param quantumCtrlsOut The qubits that quantumCtrlsIn are changed to. + * @param posClassicalCtrls The classical values that have to be true + * (nonzero) to apply the matrix. + * @param negClassicalCtrls The classical values that have to be false (zero) + * to apply the matrix. + * @return failure() if a target/control value is absent, the two targets + * coincide, the control in/out lengths mismatch, or a classical control is + * unresolved. + */ + [[nodiscard("UnionTable::applyMatrix2Q called but ignored")]] LogicalResult + applyMatrix2Q(Value in0, Value in1, Value out0, Value out1, + const Matrix4x4& matrix, ArrayRef quantumCtrlsIn = {}, + ArrayRef quantumCtrlsOut = {}, + ArrayRef posClassicalCtrls = {}, + ArrayRef negClassicalCtrls = {}); + + /** + * @brief Adds a global phase exp(i*theta), where theta is a classical value. + * + * The slot holding theta (and any controls) is coalesced, then each + * alternative resolves theta from its own constants and applies the phase - + * uncontrolled into its global phase, controlled as a relative phase. + * + * @param theta The classical value holding the rotation angle in radians. + * @param quantumCtrlsIn The qubits that have to be |1> for the phase. + * @param quantumCtrlsOut The qubits that quantumCtrlsIn are changed to. + * @param posClassicalCtrls The classical values that have to be true + * (nonzero) for the phase. + * @param negClassicalCtrls The classical values that have to be false (zero) + * for the phase. + * @return failure() if theta or a control value is absent, the control in/out + * lengths mismatch, or theta / a classical control is not a resolved constant + * where the phase would apply (each indicating a propagation bug). + */ + [[nodiscard("UnionTable::addGlobalPhase called but ignored")]] LogicalResult + addGlobalPhase(Value theta, ArrayRef quantumCtrlsIn = {}, + ArrayRef quantumCtrlsOut = {}, + ArrayRef posClassicalCtrls = {}, + ArrayRef negClassicalCtrls = {}); + + /** + * @brief Folds a classical operation across the distribution. + * + * Merges the slots of op 's tracked operands, then folds op per alternative + * with that alternative's constants, recording any constant results. A result + * that does not fold stays untracked. + * + * @param op The classical operation to propagate. + */ + void propagateClassical(Operation* op); + + /** + * @brief Measures in (renamed to out), recording the outcome in + * classicalResult. + * + * Per alternative: an exact bit if in is deterministic there, otherwise that + * alternative's QuantumState collapses to top, and the result stays unknown. + * + * @param in The qubit to be measured. + * @param out The value to change in to. + * @param classicalResult The classical value to record the outcome in. + * @param posClassicalCtrls The classical values that have to be true + * (nonzero) to apply the measurement. + * @param negClassicalCtrls The classical values that have to be false (zero) + * to apply the measurement. + * @return failure() if in is absent or a classical control is unresolved. + */ + [[nodiscard("UnionTable::measureQubit called but ignored")]] LogicalResult + measureQubit(Value in, Value out, Value classicalResult, + ArrayRef posClassicalCtrls = {}, + ArrayRef negClassicalCtrls = {}); + + /** + * @brief Resets in to |0> (renamed to out). + * + * Exact per alternative when in is deterministic there, otherwise that + * alternative's QuantumState collapses to top. + * + * @param in The qubit to be reset. + * @param out The value to change in to. + * @param posClassicalCtrls The classical values that have to be true + * (nonzero) to apply the reset. + * @param negClassicalCtrls The classical values that have to be false (zero) + * to apply the reset. + * @return failure() if in is absent or a classical control is unresolved. + */ + [[nodiscard("UnionTable::resetQubit called but ignored")]] LogicalResult + resetQubit(Value in, Value out, ArrayRef posClassicalCtrls = {}, + ArrayRef negClassicalCtrls = {}); + + /** + * @brief Collapses the QuantumState of every alternative in the slots that + * hold qubits to top. The analysis' fallback for an operation whose effect it + * cannot represent. + * + * @param qubits The qubits whose slots should collapse to top. + */ + void markQubitsTop(ArrayRef qubits); + + //===--------------------------------------------------------------------===// + // Queries + //===--------------------------------------------------------------------===// + + [[nodiscard("UnionTable::isQubitAlwaysOne called but ignored")]] bool + isQubitAlwaysOne(Value q) const; + [[nodiscard("UnionTable::isQubitAlwaysZero called but ignored")]] bool + isQubitAlwaysZero(Value q) const; + [[nodiscard("UnionTable::isClassicalAlwaysTrue called but ignored")]] bool + isClassicalAlwaysTrue(Value v) const; + [[nodiscard("UnionTable::isClassicalAlwaysFalse called but ignored")]] bool + isClassicalAlwaysFalse(Value v) const; + + /** + * @brief Whether the controls can all hold at once somewhere in the + * distribution. + * + * A conjunction over disjoint factors (each factor must be satisfiable), + * disjunction over a slot's alternatives (any alternative suffices). Controls + * absent from the table are treated as possibly satisfiable. + * + * @param quantumCtrls The qubits that have to be |1>. + * @param posClassicalCtrls The classical values that have to be true + * (nonzero). + * @param negClassicalCtrls The classical values that have to be false (zero). + * @returns True if the control configuration is satisfiable. + */ + [[nodiscard("UnionTable::areControlsSatisfiable called but ignored")]] bool + areControlsSatisfiable(ArrayRef quantumCtrls, + ArrayRef posClassicalCtrls = {}, + ArrayRef negClassicalCtrls = {}) const; + + /** + * @brief Which of the given (positive quantum / positive classical / negative + * classical) controls are redundant in the current state. + * + * @param quantumCtrls The qubits that have to be |1>. + * @param posClassicalCtrls The classical values that have to be true + * (nonzero). + * @param negClassicalCtrls The classical values that have to be false (zero). + * @returns Whether the whole operation is superfluous (controls can never be + * satisfied), plus the individual controls that always hold. + */ + [[nodiscard("UnionTable::getSuperfluousControls called but ignored")]] + SuperfluousResult + getSuperfluousControls(ArrayRef quantumCtrls, + ArrayRef posClassicalCtrls = {}, + ArrayRef negClassicalCtrls = {}) const; + + //===--------------------------------------------------------------------===// + // Lattice support + //===--------------------------------------------------------------------===// + + /** + * @brief Reconciles this state with other coming from a sibling control-flow + * path (the two branches of a non-constant qco.if). + * + * Slots are matched by qubit set. Matching slots merge their alternatives + * (probability-weighted, deduplicated, renormalized); a classical-only fact + * survives only if other asserts it too. The table collapses to allTop if the + * entanglement structure differs or a slot exceeds maxHybridStates. + * + * The caller aligns yielded SSA names (via forwardValues) before calling. + * + * @param other The UnionTable to join this with. + */ + void join(const UnionTable& other); + + /// @brief Collapses the whole table: no quantum or classical facts survive. + void markAllTop(); + + [[nodiscard("UnionTable::isAllTop called but ignored")]] bool + isAllTop() const { + return allTop; + } + + /// @brief Whether every tracked QuantumState is top (classical facts may + /// remain). + [[nodiscard("UnionTable::areStatesAllTop called but ignored")]] bool + areStatesAllTop() const; + + /// @brief Order-independent structural equality (drives lattice convergence). + [[nodiscard("UnionTable::== called but ignored")]] bool + operator==(const UnionTable& other) const; + + void print(raw_ostream& os) const; +}; + +} // namespace mlir::qco diff --git a/mlir/lib/Support/Passes.cpp b/mlir/lib/Support/Passes.cpp index 8c1302a361..9775e38cf1 100644 --- a/mlir/lib/Support/Passes.cpp +++ b/mlir/lib/Support/Passes.cpp @@ -51,6 +51,7 @@ runWithPassManager(ModuleOp mod, void registerMQTCompilerPasses() { static const auto REGISTERED = [] { registerConvertCBitToMemRef(); + qco::registerConstantPropagation(); qco::registerDecomposeMultiControlled(); qco::registerFuseSingleQubitUnitaryRuns(); qco::registerHadamardLifting(); diff --git a/mlir/unittests/Dialect/QCO/Transforms/Optimizations/CMakeLists.txt b/mlir/unittests/Dialect/QCO/Transforms/Optimizations/CMakeLists.txt index 5cc0548e3e..efe30de551 100644 --- a/mlir/unittests/Dialect/QCO/Transforms/Optimizations/CMakeLists.txt +++ b/mlir/unittests/Dialect/QCO/Transforms/Optimizations/CMakeLists.txt @@ -9,6 +9,11 @@ set(target_name mqt-core-mlir-unittest-optimizations) add_executable( ${target_name} + ConstantPropagation/test_constantPropagationAnalysis.cpp + ConstantPropagation/test_hybridState.cpp + ConstantPropagation/test_quantumState.cpp + ConstantPropagation/test_unionTable.cpp + test_qco_constant_propagation.cpp test_qco_hadamard_lifting.cpp test_qco_measurement_lifting.cpp test_qco_merge_single_qubit_rotation.cpp @@ -21,12 +26,19 @@ add_executable( target_link_libraries( ${target_name} PRIVATE GTest::gtest_main + MLIRAnalysis MLIRControlFlowDialect + MLIRControlFlowInterfaces + MLIRFunctionInterfaces + MLIRMQTDialect MLIRQCODDFunctionality MLIRQCOProgramBuilder MLIRQCOPrograms MLIRQCOTransforms MLIRQCOUtils + MLIRQTensorDialect + MLIRArithDialect + MLIRFuncDialect MLIRParser MLIRIR MLIRPass @@ -34,6 +46,10 @@ target_link_libraries( LLVMSupport MLIRSupportMQT) +# ConstantPropagation unit tests reach into the pass's private headers. +target_include_directories( + ${target_name} PRIVATE ${PROJECT_SOURCE_DIR}/mlir/lib/Dialect/QCO/Transforms/Optimizations) + mqt_mlir_configure_unittest_target(${target_name} REQUIRES_EH) gtest_discover_tests(${target_name} PROPERTIES LABELS mqt-mlir-unittests DISCOVERY_TIMEOUT 60) diff --git a/mlir/unittests/Dialect/QCO/Transforms/Optimizations/ConstantPropagation/test_constantPropagationAnalysis.cpp b/mlir/unittests/Dialect/QCO/Transforms/Optimizations/ConstantPropagation/test_constantPropagationAnalysis.cpp new file mode 100644 index 0000000000..9dd7507c25 --- /dev/null +++ b/mlir/unittests/Dialect/QCO/Transforms/Optimizations/ConstantPropagation/test_constantPropagationAnalysis.cpp @@ -0,0 +1,259 @@ +/* + * Copyright (c) 2023 - 2026 Chair for Design Automation, TUM + * Copyright (c) 2025 - 2026 Munich Quantum Software Company GmbH + * All rights reserved. + * + * SPDX-License-Identifier: MIT + * + * Licensed under the MIT License + */ + +#include "ConstantPropagation/ConstantPropagationAnalysis.hpp" +#include "mlir/Dialect/QCO/Builder/QCOProgramBuilder.h" +#include "mlir/Dialect/QCO/IR/QCODialect.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +using namespace mlir; +using namespace mlir::qco; + +/// Runs the analysis over module and returns a " -> " line +/// for every operation, in walk order. +static std::string analyze(ModuleOp module, size_t maxAmplitudes = 16, + size_t maxHybridStates = 8) { + DataFlowSolver solver; + solver.load(); + solver.load(); + solver.load(maxAmplitudes, maxHybridStates); + if (failed(solver.initializeAndRun(module))) { + return ""; + } + + std::string out; + llvm::raw_string_ostream os(out); + module.walk([&](Operation* op) { + if (isa(op)) { + return; + } + os << op->getName().getStringRef() << " -> "; + if (const auto* lattice = solver.lookupState( + solver.getProgramPointAfter(op))) { + lattice->print(os); + } else { + os << ""; + } + os << "\n"; + }); + return out; +} + +namespace { + +class ConstantPropagationAnalysisTest : public testing::Test { +protected: + MLIRContext context; + QCOProgramBuilder builder; + + ConstantPropagationAnalysisTest() : builder(&context) {} + + void SetUp() override { + DialectRegistry registry; + registry.insert(); + context.appendDialectRegistry(registry); + context.loadAllAvailableDialects(); + builder.initialize(); + } +}; + +TEST_F(ConstantPropagationAnalysisTest, allocSeedsZeroAndGateInterprets) { + auto reg = builder.allocQubitRegister(1); + builder.x(reg[0]); + const auto module = builder.finalize(); + + const std::string dump = analyze(*module); + EXPECT_EQ(dump.find(""), std::string::npos); + EXPECT_NE(dump.find("qco.x -> "), std::string::npos); + EXPECT_NE(dump.find("|1> -> 1.00"), std::string::npos); +} + +TEST_F(ConstantPropagationAnalysisTest, uncalledHelperDoesNotDisturbEntry) { + auto reg = builder.allocQubitRegister(1); + builder.x(reg[0]); + auto module = builder.finalize(); + + // An uncalled helper function is tolerated: the entry stays precise. + OpBuilder ob(module->getContext()); + ob.setInsertionPointToEnd(module->getBody()); + func::FuncOp::create(ob, module->getLoc(), "helper", + ob.getFunctionType({}, {})) + .setPrivate(); + + const std::string dump = analyze(*module); + EXPECT_EQ(dump.find(""), std::string::npos); + EXPECT_NE(dump.find("|1> -> 1.00"), std::string::npos); +} + +TEST_F(ConstantPropagationAnalysisTest, anyCallBailsToTop) { + auto reg = builder.allocQubitRegister(1); + builder.x(reg[0]); + auto module = builder.finalize(); + + OpBuilder ob(module->getContext()); + ob.setInsertionPointToEnd(module->getBody()); + auto callee = func::FuncOp::create(ob, module->getLoc(), "callee", + ob.getFunctionType({}, {})); + callee.setPrivate(); + auto entry = *module->getBody()->getOps().begin(); + ob.setInsertionPointToStart(&entry.getBody().front()); + func::CallOp::create(ob, module->getLoc(), callee, ValueRange{}); + + const std::string dump = analyze(*module); + EXPECT_EQ(dump.find(""), std::string::npos); + EXPECT_NE(dump.find("qco.x -> "), std::string::npos); +} + +TEST_F(ConstantPropagationAnalysisTest, independentGatesStayFactored) { + auto reg = builder.allocQubitRegister(2); + builder.x(reg[0]); + builder.x(reg[1]); + const auto module = builder.finalize(); + + const std::string dump = analyze(*module); + EXPECT_EQ(dump.find(""), std::string::npos); + EXPECT_EQ(dump.find("|11> -> 1.00"), std::string::npos); + EXPECT_NE(dump.find("|1> -> 1.00"), std::string::npos); +} + +TEST_F(ConstantPropagationAnalysisTest, entanglingGateMergedFactors) { + auto reg = builder.allocQubitRegister(2); + Value q0 = builder.x(reg[0]); + builder.dcx(q0, reg[1]); + const auto module = builder.finalize(); + + const std::string dump = analyze(*module); + EXPECT_EQ(dump.find(""), std::string::npos); + EXPECT_NE(dump.find("qco.dcx -> "), std::string::npos); + EXPECT_NE(dump.find("|10> -> 1.00"), std::string::npos); +} + +TEST_F(ConstantPropagationAnalysisTest, controlledGateFires) { + auto reg = builder.allocQubitRegister(2); + Value q0 = builder.h(reg[0]); + builder.cx(q0, reg[1]); + const auto module = builder.finalize(); + + const std::string dump = analyze(*module); + EXPECT_EQ(dump.find(""), std::string::npos); + EXPECT_NE(dump.find("|00> -> 0.71"), std::string::npos); + EXPECT_NE(dump.find("|11> -> 0.71"), std::string::npos); +} + +TEST_F(ConstantPropagationAnalysisTest, measuringSuperpositionTops) { + auto reg = builder.allocQubitRegister(1); + Value q0 = builder.h(reg[0]); + builder.measure(q0); + const auto module = builder.finalize(); + + const std::string dump = analyze(*module); + EXPECT_EQ(dump.find(""), std::string::npos); + // H builds a real superposition... + EXPECT_NE(dump.find("[|0> -> 0.71, |1> -> 0.71]"), std::string::npos); + // ...and measuring it tops that qubit's state (no v2.0 hybrid-state split); + // the measured qubit prints as from qco.measure onward. + EXPECT_NE(dump.find("qco.measure ->"), std::string::npos); + EXPECT_NE(dump.find("[]"), std::string::npos); +} + +TEST_F(ConstantPropagationAnalysisTest, deterministicMeasurementRecordsBit) { + auto reg = builder.allocQubitRegister(1); + Value q0 = builder.x(reg[0]); + builder.measure(q0); + const auto module = builder.finalize(); + + const std::string dump = analyze(*module); + EXPECT_EQ(dump.find(""), std::string::npos); + EXPECT_NE(dump.find("qco.measure -> "), std::string::npos); + EXPECT_NE(dump.find("classical:"), std::string::npos); +} + +TEST_F(ConstantPropagationAnalysisTest, resetIsInterpreted) { + auto reg = builder.allocQubitRegister(1); + Value q0 = builder.x(reg[0]); + builder.reset(q0); + const auto module = builder.finalize(); + + const std::string dump = analyze(*module); + EXPECT_EQ(dump.find(""), std::string::npos); + EXPECT_NE(dump.find("qco.reset -> "), std::string::npos); +} + +TEST_F(ConstantPropagationAnalysisTest, globalPhaseIsInterpreted) { + auto reg = builder.allocQubitRegister(1); + builder.x(reg[0]); + builder.gphase(0.5); + const auto module = builder.finalize(); + + const std::string dump = analyze(*module); + EXPECT_EQ(dump.find(""), std::string::npos); + EXPECT_NE(dump.find("qco.gphase -> "), std::string::npos); +} + +TEST_F(ConstantPropagationAnalysisTest, + constantIfIsThreadedThroughBothBranches) { + auto reg = builder.allocQubitRegister(1); + builder.qcoIf( + true, reg[0], [&](Value arg) { return builder.x(arg); }, + [&](Value arg) { return builder.h(arg); }); + const auto module = builder.finalize(); + + const std::string dump = analyze(*module); + EXPECT_EQ(dump.find(""), std::string::npos); + EXPECT_NE(dump.find("qco.if -> "), std::string::npos); + EXPECT_NE(dump.find("p=1.0000 [|1> -> 1.00]"), std::string::npos); +} + +TEST_F(ConstantPropagationAnalysisTest, classicalArithmeticIsFolded) { + auto reg = builder.allocQubitRegister(1); + builder.x(reg[0]); + auto module = builder.finalize(); + + OpBuilder ob(module->getContext()); + auto entry = *module->getBody()->getOps().begin(); + ob.setInsertionPointToStart(&entry.getBody().front()); + Value a = + arith::ConstantOp::create(ob, module->getLoc(), ob.getI64IntegerAttr(2)); + Value b = + arith::ConstantOp::create(ob, module->getLoc(), ob.getI64IntegerAttr(3)); + arith::AddIOp::create(ob, module->getLoc(), a, b); + + const std::string dump = analyze(*module); + EXPECT_EQ(dump.find(""), std::string::npos); + // `addi 2, 3` folds to a classical constant 5, recorded in the lattice as a + // resolved value; the state stays precise - not top, not empty. + EXPECT_NE(dump.find("arith.addi -> "), std::string::npos); + EXPECT_NE(dump.find("=5 : i64"), std::string::npos); + EXPECT_EQ(dump.find("arith.addi -> "), std::string::npos); + EXPECT_EQ(dump.find("arith.addi -> "), std::string::npos); +} + +} // namespace diff --git a/mlir/unittests/Dialect/QCO/Transforms/Optimizations/ConstantPropagation/test_hybridState.cpp b/mlir/unittests/Dialect/QCO/Transforms/Optimizations/ConstantPropagation/test_hybridState.cpp new file mode 100644 index 0000000000..2dc171b350 --- /dev/null +++ b/mlir/unittests/Dialect/QCO/Transforms/Optimizations/ConstantPropagation/test_hybridState.cpp @@ -0,0 +1,510 @@ +/* + * Copyright (c) 2023 - 2026 Chair for Design Automation, TUM + * Copyright (c) 2025 - 2026 Munich Quantum Software Company GmbH + * All rights reserved. + * + * SPDX-License-Identifier: MIT + * + * Licensed under the MIT License + */ + +#include "ConstantPropagation/HybridState.hpp" +#include "ConstantPropagation/QuantumState.hpp" +#include "mlir/Dialect/QCO/Builder/QCOProgramBuilder.h" +#include "mlir/Dialect/QCO/IR/QCODialect.h" +#include "mlir/Dialect/QCO/IR/QCOOps.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +using namespace mlir; +using namespace mlir::qco; + +static std::string printed(const HybridState& hs) { + std::string s; + llvm::raw_string_ostream os(s); + hs.print(os); + return s; +} + +namespace { + +constexpr size_t BUDGET = 16; + +class HybridStateTest : public testing::Test { +protected: + MLIRContext context; + QCOProgramBuilder builder; + + std::array q{}; + Value cA; + Value cB; + HOp hOp; + XOp xOp; + DCXOp dcxOp; + + HybridStateTest() : builder(&context) {} + + void SetUp() override { + DialectRegistry registry; + registry.insert(); + context.appendDialectRegistry(registry); + context.loadAllAvailableDialects(); + + builder.initialize(); + auto reg = builder.allocQubitRegister(4); + for (size_t i = 0; i < q.size(); ++i) { + q[i] = reg[i]; + } + cA = builder.boolConstant(false); + cB = builder.boolConstant(true); + const auto qt = q[0].getType(); + hOp = HOp::create(builder, builder.getLoc(), qt, q[0]); + xOp = XOp::create(builder, builder.getLoc(), qt, q[0]); + dcxOp = DCXOp::create(builder, builder.getLoc(), qt, qt, q[0], q[1]); + } + + static HybridState make(ArrayRef qubits, double probability = 1.0) { + return HybridState(QuantumState(qubits, BUDGET), BUDGET, probability); + } +}; + +//===----------------------------------------------------------------------===// +// Construction / classical values +//===----------------------------------------------------------------------===// + +TEST_F(HybridStateTest, holdsItsQubits) { + const auto hs = make({q[0], q[1]}); + EXPECT_TRUE(hs.hasQubit(q[0])); + EXPECT_FALSE(hs.hasQubit(q[2])); + EXPECT_EQ(hs.getQubits().size(), 2U); + EXPECT_TRUE(hs.isQubitAlwaysZero(q[0])); + EXPECT_FALSE(hs.isTop()); +} + +TEST_F(HybridStateTest, setAndGetClassical) { + auto hs = make({}); + EXPECT_FALSE(hs.getClassical(cA).has_value()); + + hs.setClassical(cA, builder.getBoolAttr(true)); + ASSERT_TRUE(hs.getClassical(cA).has_value()); + EXPECT_TRUE(hs.isClassicalTrue(cA)); + EXPECT_FALSE(hs.isClassicalFalse(cA)); + + hs.setClassical(cA, builder.getBoolAttr(false)); + EXPECT_FALSE(hs.isClassicalTrue(cA)); + EXPECT_TRUE(hs.isClassicalFalse(cA)); +} + +//===----------------------------------------------------------------------===// +// Gate application +//===----------------------------------------------------------------------===// + +TEST_F(HybridStateTest, applyMatrix1Q) { + auto hs = make({q[0]}); + ASSERT_TRUE(hs.applyMatrix1Q(q[0], q[0], hOp.getUnitaryMatrix()).succeeded()); + EXPECT_FALSE(hs.isQubitAlwaysZero(q[0])); +} + +TEST_F(HybridStateTest, applyToQubitNotInStateFails) { + auto hs = make({q[0]}); + EXPECT_TRUE(hs.applyMatrix1Q(q[1], q[1], xOp.getUnitaryMatrix()).failed()); +} + +TEST_F(HybridStateTest, applyMatrix2Q) { + auto hs = make({q[0], q[1]}); + ASSERT_TRUE(hs.applyMatrix1Q(q[0], q[0], xOp.getUnitaryMatrix()).succeeded()); + ASSERT_TRUE(hs.applyMatrix2Q(q[0], q[1], q[0], q[1], dcxOp.getUnitaryMatrix()) + .succeeded()); + EXPECT_TRUE(hs.isQubitAlwaysZero(q[0])); + EXPECT_TRUE(hs.isQubitAlwaysOne(q[1])); +} + +TEST_F(HybridStateTest, quantumControlledGate) { + auto hs = make({q[0], q[1]}); + ASSERT_TRUE(hs.applyMatrix1Q(q[0], q[0], xOp.getUnitaryMatrix()).succeeded()); + ASSERT_TRUE( + hs.applyMatrix1Q(q[1], q[1], xOp.getUnitaryMatrix(), {q[0]}, {q[0]}) + .succeeded()); + EXPECT_TRUE(hs.isQubitAlwaysOne(q[1])); +} + +TEST_F(HybridStateTest, controlRenameUpdatesState) { + auto hs = make({q[0], q[1]}); + ASSERT_TRUE( + hs.applyMatrix1Q(q[1], q[3], xOp.getUnitaryMatrix(), {q[0]}, {q[2]}) + .succeeded()); + EXPECT_FALSE(hs.hasQubit(q[0])); + EXPECT_FALSE(hs.hasQubit(q[1])); + EXPECT_TRUE(hs.hasQubit(q[2])); + EXPECT_TRUE(hs.hasQubit(q[3])); +} + +TEST_F(HybridStateTest, controlInOutLengthMismatchFails) { + auto hs = make({q[0], q[1]}); + EXPECT_TRUE( + hs.applyMatrix1Q(q[1], q[1], xOp.getUnitaryMatrix(), {q[0]}, {q[2], q[3]}) + .failed()); +} + +TEST_F(HybridStateTest, nonEmptyControlInEmptyControlOutFails) { + auto hs = make({q[0], q[1]}); + EXPECT_TRUE(hs.applyMatrix1Q(q[1], q[1], xOp.getUnitaryMatrix(), {q[0]}, {}) + .failed()); +} + +//===----------------------------------------------------------------------===// +// Classical controls +//===----------------------------------------------------------------------===// + +TEST_F(HybridStateTest, positiveClassicalControlHoldsAppliesGate) { + auto hs = make({q[0]}); + hs.setClassical(cA, builder.getBoolAttr(true)); + ASSERT_TRUE(hs.applyMatrix1Q(q[0], q[0], xOp.getUnitaryMatrix(), {}, {}, {cA}) + .succeeded()); + EXPECT_TRUE(hs.isQubitAlwaysOne(q[0])); +} + +TEST_F(HybridStateTest, positiveClassicalControlFailsSkipsGateButRenames) { + auto hs = make({q[0]}); + hs.setClassical(cA, builder.getBoolAttr(false)); + ASSERT_TRUE(hs.applyMatrix1Q(q[0], q[1], xOp.getUnitaryMatrix(), {}, {}, {cA}) + .succeeded()); + EXPECT_FALSE(hs.hasQubit(q[0])); + EXPECT_TRUE(hs.hasQubit(q[1])); + EXPECT_TRUE(hs.isQubitAlwaysZero(q[1])); +} + +TEST_F(HybridStateTest, negativeClassicalControlHoldsAppliesGate) { + auto hs = make({q[0]}); + hs.setClassical(cA, builder.getBoolAttr(false)); + ASSERT_TRUE( + hs.applyMatrix1Q(q[0], q[0], xOp.getUnitaryMatrix(), {}, {}, {}, {cA}) + .succeeded()); + EXPECT_TRUE(hs.isQubitAlwaysOne(q[0])); +} + +TEST_F(HybridStateTest, negativeClassicalControlFailsSkipsGate) { + auto hs = make({q[0]}); + hs.setClassical(cA, builder.getBoolAttr(true)); + ASSERT_TRUE( + hs.applyMatrix1Q(q[0], q[0], xOp.getUnitaryMatrix(), {}, {}, {}, {cA}) + .succeeded()); + EXPECT_TRUE(hs.isQubitAlwaysZero(q[0])); +} + +TEST_F(HybridStateTest, unresolvedClassicalControlFails) { + auto hs = make({q[0]}); + EXPECT_TRUE(hs.applyMatrix1Q(q[0], q[0], xOp.getUnitaryMatrix(), {}, {}, {cA}) + .failed()); +} + +TEST_F(HybridStateTest, floatClassicalControlIsSupported) { + auto hs = make({q[0]}); + Value fc = builder.floatConstant(2.5); + + hs.setClassical(fc, builder.getF64FloatAttr(2.5)); + EXPECT_TRUE(hs.isClassicalTrue(fc)); + ASSERT_TRUE(hs.applyMatrix1Q(q[0], q[0], xOp.getUnitaryMatrix(), {}, {}, {fc}) + .succeeded()); + EXPECT_TRUE(hs.isQubitAlwaysOne(q[0])); + + hs.setClassical(fc, builder.getF64FloatAttr(0.0)); + EXPECT_TRUE(hs.isClassicalFalse(fc)); +} + +//===----------------------------------------------------------------------===// +// Global phase +//===----------------------------------------------------------------------===// + +TEST_F(HybridStateTest, uncontrolledGlobalPhaseAccumulates) { + auto hs = make({q[0]}); + Value theta = builder.floatConstant(std::acos(-1.0)); + hs.setClassical(theta, builder.getF64FloatAttr(std::acos(-1.0))); + ASSERT_TRUE(hs.addGlobalPhase(theta).succeeded()); + EXPECT_LT(std::abs(hs.getGlobalPhase() - Complex{-1.0, 0.0}), 1e-9); +} + +TEST_F(HybridStateTest, quantumControlledPhaseIsNotGlobal) { + auto hs = make({q[0], q[1]}); + Value theta = builder.floatConstant(std::acos(-1.0)); + hs.setClassical(theta, builder.getF64FloatAttr(std::acos(-1.0))); + ASSERT_TRUE(hs.applyMatrix1Q(q[0], q[0], xOp.getUnitaryMatrix()).succeeded()); + ASSERT_TRUE(hs.addGlobalPhase(theta, {q[0]}, {q[0]}).succeeded()); + EXPECT_LT(std::abs(hs.getGlobalPhase() - Complex{1.0, 0.0}), 1e-9); +} + +TEST_F(HybridStateTest, globalPhaseSkippedByClassicalControl) { + auto hs = make({q[0]}); + Value theta = builder.floatConstant(std::acos(-1.0)); + hs.setClassical(theta, builder.getF64FloatAttr(std::acos(-1.0))); + hs.setClassical(cA, builder.getBoolAttr(false)); + ASSERT_TRUE(hs.addGlobalPhase(theta, {}, {}, {cA}).succeeded()); + EXPECT_LT(std::abs(hs.getGlobalPhase() - Complex{1.0, 0.0}), 1e-9); +} + +TEST_F(HybridStateTest, globalPhaseFailsWhenThetaUnresolved) { + auto hs = make({q[0]}); + Value theta = builder.floatConstant(std::acos(-1.0)); // never seeded + EXPECT_TRUE(hs.addGlobalPhase(theta).failed()); +} + +TEST_F(HybridStateTest, propagateClassicalFoldsConstants) { + auto hs = make({}); + Value lhs = builder.intConstant(3); + Value rhs = builder.intConstant(4); + hs.setClassical(lhs, builder.getIntegerAttr(lhs.getType(), 3)); + hs.setClassical(rhs, builder.getIntegerAttr(rhs.getType(), 4)); + auto add = arith::AddIOp::create(builder, builder.getLoc(), lhs, rhs); + hs.propagateClassical(add.getOperation()); + const auto folded = hs.getClassical(add.getResult()); + ASSERT_TRUE(folded.has_value()); + const auto intAttr = dyn_cast(*folded); + ASSERT_TRUE(intAttr); + EXPECT_EQ(intAttr.getInt(), 7); +} + +//===----------------------------------------------------------------------===// +// tensor +//===----------------------------------------------------------------------===// + +TEST_F(HybridStateTest, tensorCombinesDisjointSubsystems) { + auto a = make({q[0]}); + ASSERT_TRUE(a.applyMatrix1Q(q[0], q[0], hOp.getUnitaryMatrix()).succeeded()); + a.setClassical(cA, builder.getBoolAttr(true)); + auto b = make({q[1]}); + b.setClassical(cB, builder.getBoolAttr(false)); + + const auto ab = a.tensor(b); + EXPECT_EQ(ab.getQubits().size(), 2U); + EXPECT_TRUE(ab.hasQubit(q[0])); + EXPECT_TRUE(ab.hasQubit(q[1])); + EXPECT_TRUE(ab.isClassicalTrue(cA)); + EXPECT_TRUE(ab.isClassicalFalse(cB)); +} + +TEST_F(HybridStateTest, tensorMultipliesProbabilities) { + const auto a = make({q[0]}, 0.5); + const auto b = make({q[1]}, 0.5); + EXPECT_DOUBLE_EQ(a.tensor(b).getProbability(), 0.25); +} + +//===----------------------------------------------------------------------===// +// Measurement +//===----------------------------------------------------------------------===// + +TEST_F(HybridStateTest, measureDeterministicRecordsClassical) { + auto hs = make({q[0]}); + ASSERT_TRUE(hs.applyMatrix1Q(q[0], q[0], xOp.getUnitaryMatrix()).succeeded()); + ASSERT_TRUE(hs.measureQubit(q[0], q[1], cA).succeeded()); + EXPECT_TRUE(hs.isClassicalTrue(cA)); + EXPECT_TRUE(hs.hasQubit(q[1])); + EXPECT_FALSE(hs.hasQubit(q[0])); +} + +TEST_F(HybridStateTest, measureSuperpositionTopsAndLeavesResultUnknown) { + auto hs = make({q[0]}); + ASSERT_TRUE(hs.applyMatrix1Q(q[0], q[0], hOp.getUnitaryMatrix()).succeeded()); + ASSERT_TRUE(hs.measureQubit(q[0], q[1], cA).succeeded()); + EXPECT_FALSE(hs.getClassical(cA).has_value()); + EXPECT_TRUE(hs.isTop()); +} + +TEST_F(HybridStateTest, measureSkippedByClassicalControl) { + auto hs = make({q[0]}); + hs.setClassical(cB, builder.getBoolAttr(false)); + ASSERT_TRUE(hs.applyMatrix1Q(q[0], q[0], xOp.getUnitaryMatrix()).succeeded()); + ASSERT_TRUE(hs.measureQubit(q[0], q[1], cA, {cB}).succeeded()); + EXPECT_FALSE(hs.getClassical(cA).has_value()); + EXPECT_TRUE(hs.hasQubit(q[1])); +} + +TEST_F(HybridStateTest, measureUnseededFails) { + auto hs = make({q[0]}); + EXPECT_TRUE(hs.measureQubit(q[1], q[2], cA).failed()); +} + +//===----------------------------------------------------------------------===// +// Reset +//===----------------------------------------------------------------------===// + +TEST_F(HybridStateTest, resetSingletonForcesZero) { + auto hs = make({q[0]}); + ASSERT_TRUE(hs.applyMatrix1Q(q[0], q[0], xOp.getUnitaryMatrix()).succeeded()); + ASSERT_TRUE(hs.resetQubit(q[0], q[1]).succeeded()); + EXPECT_TRUE(hs.isQubitAlwaysZero(q[1])); +} + +TEST_F(HybridStateTest, resetDeterministicOneInLargerState) { + auto hs = make({q[0], q[1]}); + ASSERT_TRUE(hs.applyMatrix1Q(q[0], q[0], xOp.getUnitaryMatrix()).succeeded()); + ASSERT_TRUE( + hs.applyMatrix1Q(q[1], q[1], xOp.getUnitaryMatrix(), {q[0]}, {q[0]}) + .succeeded()); + ASSERT_TRUE(hs.resetQubit(q[0], q[2]).succeeded()); + EXPECT_TRUE(hs.isQubitAlwaysZero(q[2])); + EXPECT_TRUE(hs.isQubitAlwaysOne(q[1])); + EXPECT_FALSE(hs.isTop()); +} + +TEST_F(HybridStateTest, resetSuperpositionInLargerStateTops) { + auto hs = make({q[0], q[1]}); + ASSERT_TRUE(hs.applyMatrix1Q(q[0], q[0], hOp.getUnitaryMatrix()).succeeded()); + ASSERT_TRUE( + hs.applyMatrix1Q(q[1], q[1], xOp.getUnitaryMatrix(), {q[0]}, {q[0]}) + .succeeded()); + ASSERT_TRUE(hs.resetQubit(q[0], q[2]).succeeded()); + EXPECT_TRUE(hs.isTop()); +} + +TEST_F(HybridStateTest, resetUnseededFails) { + auto hs = make({q[0]}); + EXPECT_TRUE(hs.resetQubit(q[1], q[2]).failed()); +} + +//===----------------------------------------------------------------------===// +// Control satisfiability +//===----------------------------------------------------------------------===// + +TEST_F(HybridStateTest, controlsSatisfiableClassical) { + auto hs = make({}); + hs.setClassical(cA, builder.getBoolAttr(true)); + hs.setClassical(cB, builder.getBoolAttr(false)); + EXPECT_TRUE(hs.areControlsSatisfiable({}, {cA}, {cB})); + EXPECT_FALSE(hs.areControlsSatisfiable({}, {cB}, {})); + EXPECT_FALSE(hs.areControlsSatisfiable({}, {}, {cA})); +} + +TEST_F(HybridStateTest, controlsSatisfiableQuantum) { + auto hs = make({q[0], q[1]}); + ASSERT_TRUE(hs.applyMatrix1Q(q[0], q[0], xOp.getUnitaryMatrix()).succeeded()); + EXPECT_TRUE(hs.areControlsSatisfiable({q[0]}, {}, {})); + EXPECT_FALSE(hs.areControlsSatisfiable({q[1]}, {}, {})); + + ASSERT_TRUE(hs.applyMatrix1Q(q[1], q[1], xOp.getUnitaryMatrix()).succeeded()); + EXPECT_TRUE(hs.areControlsSatisfiable({q[0], q[1]}, {}, {})); +} + +TEST_F(HybridStateTest, controlsSatisfiableQubitNotInStateIsFalse) { + const auto hs = make({q[0]}); + EXPECT_FALSE(hs.areControlsSatisfiable({q[1]}, {}, {})); +} + +//===----------------------------------------------------------------------===// +// Equality / print +//===----------------------------------------------------------------------===// + +TEST_F(HybridStateTest, equalityConsidersEverything) { + auto a = make({q[0]}, 0.5); + auto b = make({q[0]}, 0.5); + EXPECT_TRUE(a == b); + + EXPECT_FALSE(a == make({q[0]}, 0.25)); + + b.setClassical(cA, builder.getBoolAttr(true)); + EXPECT_FALSE(a == b); + + auto c = make({q[0]}, 0.5); + ASSERT_TRUE(c.applyMatrix1Q(q[0], q[0], xOp.getUnitaryMatrix()).succeeded()); + EXPECT_FALSE(a == c); +} + +TEST_F(HybridStateTest, sameConfigurationIgnoresProbability) { + auto a = make({q[0]}, 0.5); + const auto b = make({q[0]}, 0.25); + EXPECT_TRUE(a.sameConfiguration(b)); + EXPECT_FALSE(a == b); + + a.setClassical(cA, builder.getBoolAttr(true)); + EXPECT_FALSE(a.sameConfiguration(b)); +} + +TEST_F(HybridStateTest, setProbabilityReplacesTheWeight) { + auto hs = make({q[0]}, 0.5); + hs.setProbability(0.2); + EXPECT_DOUBLE_EQ(hs.getProbability(), 0.2); +} + +TEST_F(HybridStateTest, markStateTopKeepsClassicalFacts) { + auto hs = make({q[0]}); + hs.setClassical(cA, builder.getBoolAttr(true)); + hs.markStateTop(); + EXPECT_TRUE(hs.isTop()); + EXPECT_TRUE(hs.isClassicalTrue(cA)); +} + +TEST_F(HybridStateTest, intersectClassicalKeepsOnlyAgreedFacts) { + auto a = make({q[0]}); + a.setClassical(cA, builder.getBoolAttr(true)); + a.setClassical(cB, builder.getBoolAttr(true)); + + auto b = make({q[0]}); + b.setClassical(cA, builder.getBoolAttr(true)); // agrees + b.setClassical(cB, builder.getBoolAttr(false)); // disagrees + + a.intersectClassical(b); + EXPECT_TRUE(a.isClassicalTrue(cA)); + EXPECT_FALSE(a.getClassical(cB).has_value()); +} + +TEST_F(HybridStateTest, forwardValueRenamesQubitAndClassical) { + auto hs = make({q[0]}); + hs.setClassical(cA, builder.getBoolAttr(true)); + hs.forwardValue(q[0], q[1]); + hs.forwardValue(cA, cB); + EXPECT_FALSE(hs.hasQubit(q[0])); + EXPECT_TRUE(hs.hasQubit(q[1])); + EXPECT_FALSE(hs.getClassical(cA).has_value()); + EXPECT_TRUE(hs.isClassicalTrue(cB)); +} + +TEST_F(HybridStateTest, printIsNonEmpty) { + auto hs = make({q[0]}); + hs.setClassical(cA, builder.getBoolAttr(false)); + EXPECT_NE(printed(hs).find("p=1.0000"), std::string::npos); +} + +TEST_F(HybridStateTest, globalPhaseAcceptsIntegerTheta) { + auto hs = make({q[0]}); + Value theta = builder.intConstant(3); + hs.setClassical(theta, builder.getIntegerAttr(theta.getType(), 3)); + ASSERT_TRUE(hs.addGlobalPhase(theta).succeeded()); + EXPECT_LT(std::abs(hs.getGlobalPhase() - std::polar(1.0, 3.0)), 1e-9); +} + +TEST_F(HybridStateTest, + twoQubitGateSkippedByFalseClassicalControlStillForwards) { + auto hs = make({q[0], q[1]}); + ASSERT_TRUE(hs.applyMatrix1Q(q[1], q[1], xOp.getUnitaryMatrix()).succeeded()); + + hs.setClassical(cA, builder.getBoolAttr(false)); + ASSERT_TRUE(hs.applyMatrix2Q(q[0], q[1], q[2], q[3], dcxOp.getUnitaryMatrix(), + {}, {}, {cA}) + .succeeded()); + EXPECT_FALSE(hs.hasQubit(q[0])); + EXPECT_TRUE(hs.hasQubit(q[2])); + EXPECT_TRUE(hs.hasQubit(q[3])); + EXPECT_TRUE(hs.isQubitAlwaysZero(q[2])); + EXPECT_TRUE(hs.isQubitAlwaysOne(q[3])); +} + +TEST_F(HybridStateTest, unresolvedNegativeClassicalControlFails) { + auto hs = make({q[0]}); + EXPECT_TRUE( + hs.applyMatrix1Q(q[0], q[0], xOp.getUnitaryMatrix(), {}, {}, {}, {cA}) + .failed()); +} + +} // namespace diff --git a/mlir/unittests/Dialect/QCO/Transforms/Optimizations/ConstantPropagation/test_quantumState.cpp b/mlir/unittests/Dialect/QCO/Transforms/Optimizations/ConstantPropagation/test_quantumState.cpp new file mode 100644 index 0000000000..06d2b447a9 --- /dev/null +++ b/mlir/unittests/Dialect/QCO/Transforms/Optimizations/ConstantPropagation/test_quantumState.cpp @@ -0,0 +1,522 @@ +/* + * Copyright (c) 2023 - 2026 Chair for Design Automation, TUM + * Copyright (c) 2025 - 2026 Munich Quantum Software Company GmbH + * All rights reserved. + * + * SPDX-License-Identifier: MIT + * + * Licensed under the MIT License + */ + +#include "ConstantPropagation/QuantumState.hpp" +#include "mlir/Dialect/QCO/Builder/QCOProgramBuilder.h" +#include "mlir/Dialect/QCO/IR/QCODialect.h" +#include "mlir/Dialect/QCO/IR/QCOOps.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +using namespace mlir; +using namespace mlir::qco; + +/// Renders a QuantumState through its print() method for readable assertions. +static std::string printed(const QuantumState& qs) { + std::string s; + llvm::raw_string_ostream os(s); + qs.print(os); + return s; +} + +namespace { + +class QuantumStateTest : public testing::Test { +protected: + MLIRContext context; + QCOProgramBuilder builder; + + std::array q{}; + HOp hOp; + XOp xOp; + ZOp zOp; + SWAPOp swapOp; + DCXOp dcxOp; + + QuantumStateTest() : builder(&context) {} + + void SetUp() override { + DialectRegistry registry; + registry.insert(); + context.appendDialectRegistry(registry); + context.loadAllAvailableDialects(); + + builder.initialize(); + auto reg = builder.allocQubitRegister(4); + for (size_t i = 0; i < q.size(); ++i) { + q[i] = reg[i]; + } + const auto qt = q[0].getType(); + hOp = HOp::create(builder, builder.getLoc(), qt, q[0]); + xOp = XOp::create(builder, builder.getLoc(), qt, q[0]); + zOp = ZOp::create(builder, builder.getLoc(), qt, q[0]); + swapOp = SWAPOp::create(builder, builder.getLoc(), qt, qt, q[0], q[1]); + dcxOp = DCXOp::create(builder, builder.getLoc(), qt, qt, q[0], q[1]); + } +}; + +//===----------------------------------------------------------------------===// +// Construction +//===----------------------------------------------------------------------===// + +TEST_F(QuantumStateTest, allZeroState) { + const auto qs = QuantumState({q[0], q[1], q[2], q[3]}, 4); + EXPECT_FALSE(qs.isTop()); + EXPECT_EQ(qs.getQubits().size(), 4U); + EXPECT_EQ(printed(qs), "|0000> -> 1.00"); +} + +//===----------------------------------------------------------------------===// +// Single-qubit gates +//===----------------------------------------------------------------------===// + +TEST_F(QuantumStateTest, applyH) { + auto qs = QuantumState::singletonZero(q[0], 4); + ASSERT_TRUE(qs.applyMatrix1Q(q[0], q[0], hOp.getUnitaryMatrix()).succeeded()); + EXPECT_EQ(printed(qs), "|0> -> 0.71, |1> -> 0.71"); +} + +TEST_F(QuantumStateTest, applyHToThirdQubit) { + auto qs = QuantumState({q[0], q[1], q[2], q[3]}, 4); + ASSERT_TRUE(qs.applyMatrix1Q(q[2], q[2], hOp.getUnitaryMatrix()).succeeded()); + EXPECT_EQ(printed(qs), "|0000> -> 0.71, |0100> -> 0.71"); +} + +TEST_F(QuantumStateTest, applyHTwiceIsIdentity) { + auto qs = QuantumState({q[0], q[1], q[2], q[3]}, 4); + ASSERT_TRUE(qs.applyMatrix1Q(q[2], q[2], hOp.getUnitaryMatrix()).succeeded()); + ASSERT_TRUE(qs.applyMatrix1Q(q[2], q[2], hOp.getUnitaryMatrix()).succeeded()); + EXPECT_EQ(printed(qs), "|0000> -> 1.00"); +} + +TEST_F(QuantumStateTest, applyHThenZ) { + auto qs = QuantumState({q[0], q[1], q[2], q[3]}, 4); + ASSERT_TRUE(qs.applyMatrix1Q(q[2], q[2], hOp.getUnitaryMatrix()).succeeded()); + ASSERT_TRUE(qs.applyMatrix1Q(q[2], q[2], zOp.getUnitaryMatrix()).succeeded()); + EXPECT_EQ(printed(qs), "|0000> -> 0.71, |0100> -> -0.71"); +} + +TEST_F(QuantumStateTest, applyHZHIsX) { + auto qs = QuantumState({q[0], q[1], q[2], q[3]}, 4); + ASSERT_TRUE(qs.applyMatrix1Q(q[2], q[2], hOp.getUnitaryMatrix()).succeeded()); + ASSERT_TRUE(qs.applyMatrix1Q(q[2], q[2], zOp.getUnitaryMatrix()).succeeded()); + ASSERT_TRUE(qs.applyMatrix1Q(q[2], q[2], hOp.getUnitaryMatrix()).succeeded()); + EXPECT_EQ(printed(qs), "|0100> -> 1.00"); +} + +TEST_F(QuantumStateTest, applyGatesToTwoIndependentQubits) { + auto qs = QuantumState({q[0], q[1], q[2], q[3]}, 4); + ASSERT_TRUE(qs.applyMatrix1Q(q[2], q[2], hOp.getUnitaryMatrix()).succeeded()); + ASSERT_TRUE(qs.applyMatrix1Q(q[0], q[0], xOp.getUnitaryMatrix()).succeeded()); + EXPECT_EQ(printed(qs), "|0001> -> 0.71, |0101> -> 0.71"); +} + +TEST_F(QuantumStateTest, forwardQubitRenamesInPlace) { + auto qs = QuantumState::singletonZero(q[0], 4); + ASSERT_TRUE(qs.applyMatrix1Q(q[0], q[1], xOp.getUnitaryMatrix()).succeeded()); + EXPECT_FALSE(qs.contains(q[0])); + EXPECT_TRUE(qs.contains(q[1])); + EXPECT_TRUE(qs.isAlwaysOne(q[1])); +} + +//===----------------------------------------------------------------------===// +// Two-qubit gates +//===----------------------------------------------------------------------===// + +TEST_F(QuantumStateTest, applySwap) { + auto qs = QuantumState({q[0], q[1], q[2], q[3]}, 4); + ASSERT_TRUE(qs.applyMatrix1Q(q[1], q[1], hOp.getUnitaryMatrix()).succeeded()); + ASSERT_TRUE( + qs.applyMatrix2Q(q[1], q[3], q[1], q[3], swapOp.getUnitaryMatrix()) + .succeeded()); + EXPECT_EQ(printed(qs), "|0000> -> 0.71, |1000> -> 0.71"); +} + +TEST_F(QuantumStateTest, applyDcxActsAsCxCx) { + auto qs = QuantumState({q[0], q[1]}, 4); + ASSERT_TRUE(qs.applyMatrix1Q(q[0], q[0], xOp.getUnitaryMatrix()).succeeded()); + ASSERT_TRUE(qs.applyMatrix2Q(q[0], q[1], q[0], q[1], dcxOp.getUnitaryMatrix()) + .succeeded()); + EXPECT_TRUE(qs.isAlwaysZero(q[0])); + EXPECT_TRUE(qs.isAlwaysOne(q[1])); + EXPECT_EQ(printed(qs), "|10> -> 1.00"); +} + +//===----------------------------------------------------------------------===// +// Precondition failures +//===----------------------------------------------------------------------===// + +TEST_F(QuantumStateTest, applyToQubitNotInGroupFails) { + auto qs = QuantumState::singletonZero(q[0], 4); + EXPECT_TRUE(qs.applyMatrix1Q(q[1], q[1], xOp.getUnitaryMatrix()).failed()); +} + +TEST_F(QuantumStateTest, applyTwoQubitGateToSameBitFails) { + auto qs = QuantumState({q[0], q[1]}, 4); + EXPECT_TRUE( + qs.applyMatrix2Q(q[0], q[0], q[0], q[0], swapOp.getUnitaryMatrix()) + .failed()); +} + +TEST_F(QuantumStateTest, applyWithControlNotInGroupFails) { + auto qs = QuantumState({q[0], q[1]}, 4); + EXPECT_TRUE( + qs.applyMatrix1Q(q[0], q[0], xOp.getUnitaryMatrix(), {q[2]}, {q[2]}) + .failed()); +} + +TEST_F(QuantumStateTest, applyToQubitNotInGroupFailsEvenWhenTop) { + auto qs = QuantumState({q[0], q[1], q[2], q[3]}, 1); + ASSERT_TRUE(qs.applyMatrix1Q(q[0], q[0], hOp.getUnitaryMatrix()).succeeded()); + ASSERT_TRUE(qs.isTop()); + EXPECT_TRUE(qs.applyMatrix1Q(q[0], q[0], xOp.getUnitaryMatrix()).succeeded()); + Value stranger = builder.allocQubit(); + EXPECT_TRUE( + qs.applyMatrix1Q(stranger, stranger, xOp.getUnitaryMatrix()).failed()); +} + +TEST_F(QuantumStateTest, twoQubitGateWithControlNotInGroupFails) { + auto qs = QuantumState({q[0], q[1]}, 4); + EXPECT_TRUE(qs.applyMatrix2Q(q[0], q[1], q[0], q[1], + swapOp.getUnitaryMatrix(), {q[2]}, {q[2]}) + .failed()); +} + +//===----------------------------------------------------------------------===// +// Controls +//===----------------------------------------------------------------------===// + +TEST_F(QuantumStateTest, controlledGateFires) { + auto qs = QuantumState({q[0], q[1]}, 4); + ASSERT_TRUE(qs.applyMatrix1Q(q[0], q[0], xOp.getUnitaryMatrix()).succeeded()); + ASSERT_TRUE( + qs.applyMatrix1Q(q[1], q[1], xOp.getUnitaryMatrix(), {q[0]}, {q[0]}) + .succeeded()); + EXPECT_EQ(printed(qs), "|11> -> 1.00"); +} + +TEST_F(QuantumStateTest, controlledGateDoesNotFire) { + auto qs = QuantumState({q[0], q[1]}, 4); + ASSERT_TRUE( + qs.applyMatrix1Q(q[1], q[1], xOp.getUnitaryMatrix(), {q[0]}, {q[0]}) + .succeeded()); + EXPECT_EQ(printed(qs), "|00> -> 1.00"); +} + +TEST_F(QuantumStateTest, controlledGateOnSuperposition) { + auto qs = QuantumState({q[0], q[1]}, 4); + ASSERT_TRUE(qs.applyMatrix1Q(q[0], q[0], hOp.getUnitaryMatrix()).succeeded()); + ASSERT_TRUE( + qs.applyMatrix1Q(q[1], q[1], xOp.getUnitaryMatrix(), {q[0]}, {q[0]}) + .succeeded()); + EXPECT_EQ(printed(qs), "|00> -> 0.71, |11> -> 0.71"); +} + +TEST_F(QuantumStateTest, appliedGateRenamesControls) { + auto qs = QuantumState({q[0], q[1]}, 4); + ASSERT_TRUE(qs.applyMatrix1Q(q[0], q[0], xOp.getUnitaryMatrix()).succeeded()); + ASSERT_TRUE( + qs.applyMatrix1Q(q[1], q[1], xOp.getUnitaryMatrix(), {q[0]}, {q[2]}) + .succeeded()); + EXPECT_FALSE(qs.contains(q[0])); + EXPECT_TRUE(qs.contains(q[2])); + EXPECT_TRUE(qs.isAlwaysOne(q[1])); +} + +TEST_F(QuantumStateTest, controlInOutLengthMismatchFails) { + auto qs = QuantumState({q[0], q[1], q[2], q[3]}, 4); + EXPECT_TRUE( + qs.applyMatrix1Q(q[0], q[0], xOp.getUnitaryMatrix(), {q[1]}, {q[2], q[3]}) + .failed()); +} + +TEST_F(QuantumStateTest, nonEmptyControlInEmptyControlOutFails) { + auto qs = QuantumState({q[0], q[1]}, 4); + EXPECT_TRUE(qs.applyMatrix1Q(q[0], q[0], xOp.getUnitaryMatrix(), {q[1]}, {}) + .failed()); +} + +//===----------------------------------------------------------------------===// +// Amplitude budget +//===----------------------------------------------------------------------===// + +TEST_F(QuantumStateTest, exceedingAmplitudeBudgetBecomesTop) { + auto qs = QuantumState({q[0], q[1], q[2], q[3]}, 2); + ASSERT_TRUE(qs.applyMatrix1Q(q[3], q[3], hOp.getUnitaryMatrix()).succeeded()); + ASSERT_TRUE( + qs.applyMatrix1Q(q[2], q[2], xOp.getUnitaryMatrix(), {q[3]}, {q[3]}) + .succeeded()); + EXPECT_FALSE(qs.isTop()); + ASSERT_TRUE(qs.applyMatrix1Q(q[2], q[2], hOp.getUnitaryMatrix()).succeeded()); + EXPECT_TRUE(qs.isTop()); +} + +TEST_F(QuantumStateTest, topStateStillForwardsQubits) { + auto qs = QuantumState({q[0], q[1], q[2], q[3]}, 1); + ASSERT_TRUE(qs.applyMatrix1Q(q[0], q[0], hOp.getUnitaryMatrix()).succeeded()); + ASSERT_TRUE(qs.isTop()); + ASSERT_TRUE(qs.applyMatrix1Q(q[0], q[1], xOp.getUnitaryMatrix()).succeeded()); + EXPECT_FALSE(qs.contains(q[0])); + EXPECT_TRUE(qs.contains(q[1])); +} + +TEST_F(QuantumStateTest, groupWiderThanTheIndexTypeIsTop) { + auto reg = builder.allocQubitRegister(64); + SmallVector many; + for (size_t i = 0; i < 64; ++i) { + many.push_back(reg[i]); + } + const auto qs = QuantumState(many, 4); + EXPECT_TRUE(qs.isTop()); +} + +//===----------------------------------------------------------------------===// +// Controlled phase +//===----------------------------------------------------------------------===// + +TEST_F(QuantumStateTest, uncontrolledPhaseFails) { + auto qs = QuantumState::singletonZero(q[0], 4); + EXPECT_TRUE(qs.applyControlledPhase(std::acos(-1.0), {}).failed()); +} + +TEST_F(QuantumStateTest, controlledPhaseAffectsOnlyControlledSubspace) { + auto qs = QuantumState({q[0], q[1], q[2], q[3]}, 4); + ASSERT_TRUE(qs.applyMatrix1Q(q[0], q[0], hOp.getUnitaryMatrix()).succeeded()); + ASSERT_TRUE( + qs.applyControlledPhase(std::acos(-1.0), {q[0]}, {q[0]}).succeeded()); + EXPECT_EQ(printed(qs), "|0000> -> 0.71, |0001> -> -0.71"); +} + +TEST_F(QuantumStateTest, controlledPhaseOnQubitNotInGroupFails) { + auto qs = QuantumState::singletonZero(q[0], 4); + EXPECT_TRUE(qs.applyControlledPhase(1.0, {q[1]}).failed()); +} + +//===----------------------------------------------------------------------===// +// Measurement +//===----------------------------------------------------------------------===// + +TEST_F(QuantumStateTest, measureDeterministicZero) { + auto qs = QuantumState::singletonZero(q[0], 2); + const auto result = qs.measure(q[0], q[0]); + ASSERT_TRUE(succeeded(result)); + const auto& outcomes = *result; + ASSERT_EQ(outcomes.size(), 1U); + EXPECT_EQ(outcomes[0].bit, 0U); + EXPECT_DOUBLE_EQ(outcomes[0].probability, 1.0); + EXPECT_TRUE(*outcomes[0].state == qs); +} + +TEST_F(QuantumStateTest, measureRenamesMeasuredQubit) { + auto qs = QuantumState::singletonZero(q[0], 2); + const auto result = qs.measure(q[0], q[1]); + ASSERT_TRUE(succeeded(result)); + ASSERT_EQ(result->size(), 1U); + EXPECT_FALSE(result->front().state->contains(q[0])); + EXPECT_TRUE(result->front().state->contains(q[1])); +} + +TEST_F(QuantumStateTest, measureDeterministicOne) { + auto qs = QuantumState::singletonZero(q[0], 2); + ASSERT_TRUE(qs.applyMatrix1Q(q[0], q[0], xOp.getUnitaryMatrix()).succeeded()); + const auto result = qs.measure(q[0], q[0]); + ASSERT_TRUE(succeeded(result)); + const auto& outcomes = *result; + ASSERT_EQ(outcomes.size(), 1U); + EXPECT_EQ(outcomes[0].bit, 1U); + EXPECT_DOUBLE_EQ(outcomes[0].probability, 1.0); + EXPECT_TRUE(*outcomes[0].state == qs); +} + +TEST_F(QuantumStateTest, measureSuperpositionSplits) { + auto qs = QuantumState({q[0], q[1]}, 4); + ASSERT_TRUE(qs.applyMatrix1Q(q[0], q[0], hOp.getUnitaryMatrix()).succeeded()); + ASSERT_TRUE( + qs.applyMatrix1Q(q[1], q[1], xOp.getUnitaryMatrix(), {q[0]}, {q[0]}) + .succeeded()); + const auto result = qs.measure(q[0], q[0]); + ASSERT_TRUE(succeeded(result)); + const auto& outcomes = *result; + ASSERT_EQ(outcomes.size(), 2U); + EXPECT_EQ(outcomes[0].bit, 0U); + EXPECT_DOUBLE_EQ(outcomes[0].probability, 0.5); + EXPECT_EQ(printed(*outcomes[0].state), "|00> -> 1.00"); + EXPECT_EQ(outcomes[1].bit, 1U); + EXPECT_DOUBLE_EQ(outcomes[1].probability, 0.5); + EXPECT_EQ(printed(*outcomes[1].state), "|11> -> 1.00"); +} + +TEST_F(QuantumStateTest, measureQubitNotInGroupFails) { + auto qs = QuantumState::singletonZero(q[0], 2); + EXPECT_TRUE(failed(qs.measure(q[1], q[1]))); +} + +TEST_F(QuantumStateTest, measureOnTopStateYieldsNoBranches) { + auto qs = QuantumState({q[0], q[1], q[2], q[3]}, 1); + ASSERT_TRUE(qs.applyMatrix1Q(q[0], q[0], hOp.getUnitaryMatrix()).succeeded()); + ASSERT_TRUE(qs.isTop()); + const auto result = qs.measure(q[0], q[0]); + ASSERT_TRUE(succeeded(result)); + EXPECT_TRUE(result->empty()); +} + +//===----------------------------------------------------------------------===// +// Reset +//===----------------------------------------------------------------------===// + +TEST_F(QuantumStateTest, resetDeterministicZero) { + auto qs = QuantumState::singletonZero(q[0], 2); + const auto result = qs.reset(q[0], q[1]); + ASSERT_TRUE(succeeded(result)); + const auto& outcomes = *result; + ASSERT_EQ(outcomes.size(), 1U); + EXPECT_EQ(outcomes[0].bit, 0U); + EXPECT_EQ(printed(*outcomes[0].state), "|0> -> 1.00"); + EXPECT_FALSE(outcomes[0].state->contains(q[0])); + EXPECT_TRUE(outcomes[0].state->contains(q[1])); +} + +TEST_F(QuantumStateTest, resetDeterministicOneForcesZero) { + auto qs = QuantumState::singletonZero(q[0], 2); + ASSERT_TRUE(qs.applyMatrix1Q(q[0], q[0], xOp.getUnitaryMatrix()).succeeded()); + const auto result = qs.reset(q[0], q[0]); + ASSERT_TRUE(succeeded(result)); + const auto& outcomes = *result; + ASSERT_EQ(outcomes.size(), 1U); + EXPECT_EQ(outcomes[0].bit, 1U); + EXPECT_EQ(printed(*outcomes[0].state), "|0> -> 1.00"); +} + +TEST_F(QuantumStateTest, resetSuperpositionForcesTargetToZero) { + auto qs = QuantumState({q[0], q[1]}, 4); + ASSERT_TRUE(qs.applyMatrix1Q(q[0], q[0], hOp.getUnitaryMatrix()).succeeded()); + ASSERT_TRUE( + qs.applyMatrix1Q(q[1], q[1], xOp.getUnitaryMatrix(), {q[0]}, {q[0]}) + .succeeded()); + const auto result = qs.reset(q[0], q[0]); + ASSERT_TRUE(succeeded(result)); + const auto& outcomes = *result; + ASSERT_EQ(outcomes.size(), 2U); + EXPECT_EQ(printed(*outcomes[0].state), "|00> -> 1.00"); + EXPECT_DOUBLE_EQ(outcomes[1].probability, 0.5); + EXPECT_EQ(printed(*outcomes[1].state), "|10> -> 1.00"); +} + +TEST_F(QuantumStateTest, resetQubitNotInGroupFails) { + auto qs = QuantumState::singletonZero(q[0], 2); + EXPECT_TRUE(failed(qs.reset(q[1], q[1]))); +} + +TEST_F(QuantumStateTest, resetOnTopStateYieldsNoBranches) { + auto qs = QuantumState({q[0], q[1], q[2], q[3]}, 1); + ASSERT_TRUE(qs.applyMatrix1Q(q[0], q[0], hOp.getUnitaryMatrix()).succeeded()); + ASSERT_TRUE(qs.isTop()); + const auto result = qs.reset(q[0], q[0]); + ASSERT_TRUE(succeeded(result)); + EXPECT_TRUE(result->empty()); +} + +//===----------------------------------------------------------------------===// +// unify +//===----------------------------------------------------------------------===// + +TEST_F(QuantumStateTest, unifyTensorsTwoGroups) { + auto a = QuantumState::singletonZero(q[0], 10); + ASSERT_TRUE(a.applyMatrix1Q(q[0], q[0], hOp.getUnitaryMatrix()).succeeded()); + const auto b = QuantumState::singletonZero(q[1], 10); + const auto unified = a.unify(b); + EXPECT_EQ(unified.getQubits().size(), 2U); + EXPECT_EQ(printed(unified), "|00> -> 0.71, |01> -> 0.71"); +} + +TEST_F(QuantumStateTest, unifyExceedingBudgetIsTop) { + auto a = QuantumState({q[0], q[1]}, 3); + ASSERT_TRUE(a.applyMatrix1Q(q[0], q[0], hOp.getUnitaryMatrix()).succeeded()); + auto b = QuantumState({q[2], q[3]}, 3); + ASSERT_TRUE(b.applyMatrix1Q(q[2], q[2], hOp.getUnitaryMatrix()).succeeded()); + EXPECT_FALSE(a.isTop()); + EXPECT_FALSE(b.isTop()); + EXPECT_TRUE(a.unify(b).isTop()); +} + +//===----------------------------------------------------------------------===// +// Queries +//===----------------------------------------------------------------------===// + +TEST_F(QuantumStateTest, alwaysZeroAndAlwaysOne) { + auto qs = QuantumState({q[0], q[1]}, 4); + ASSERT_TRUE(qs.applyMatrix1Q(q[0], q[0], xOp.getUnitaryMatrix()).succeeded()); + EXPECT_TRUE(qs.isAlwaysOne(q[0])); + EXPECT_FALSE(qs.isAlwaysZero(q[0])); + EXPECT_TRUE(qs.isAlwaysZero(q[1])); + EXPECT_FALSE(qs.isAlwaysOne(q[1])); + + ASSERT_TRUE(qs.applyMatrix1Q(q[1], q[1], hOp.getUnitaryMatrix()).succeeded()); + EXPECT_FALSE(qs.isAlwaysZero(q[1])); + EXPECT_FALSE(qs.isAlwaysOne(q[1])); +} + +TEST_F(QuantumStateTest, hasAlwaysZeroAmplitude) { + auto qs = QuantumState({q[0], q[1]}, 4); + ASSERT_TRUE(qs.applyMatrix1Q(q[0], q[0], hOp.getUnitaryMatrix()).succeeded()); + ASSERT_TRUE( + qs.applyMatrix1Q(q[1], q[1], xOp.getUnitaryMatrix(), {q[0]}, {q[0]}) + .succeeded()); + EXPECT_TRUE(qs.hasAlwaysZeroAmplitude({{q[0], false}, {q[1], true}})); + EXPECT_FALSE(qs.hasAlwaysZeroAmplitude({{q[0], true}, {q[1], true}})); +} + +//===----------------------------------------------------------------------===// +// Equality +//===----------------------------------------------------------------------===// + +TEST_F(QuantumStateTest, equalityIgnoresNegligibleDifferences) { + auto a = QuantumState({q[0], q[1], q[2], q[3]}, 4); + const auto b = QuantumState({q[0], q[1], q[2], q[3]}, 4); + ASSERT_TRUE(a.applyMatrix1Q(q[0], q[0], hOp.getUnitaryMatrix()).succeeded()); + ASSERT_TRUE(a.applyMatrix1Q(q[0], q[0], hOp.getUnitaryMatrix()).succeeded()); + EXPECT_TRUE(a == b); +} + +TEST_F(QuantumStateTest, topStatesAreEqual) { + auto a = QuantumState({q[0], q[1], q[2], q[3]}, 1); + auto b = QuantumState({q[0], q[1], q[2], q[3]}, 1); + a.markTop(); + b.markTop(); + EXPECT_TRUE(a == b); + EXPECT_FALSE(a == QuantumState({q[0], q[1], q[2], q[3]}, 4)); +} + +//===----------------------------------------------------------------------===// +// Printing +//===----------------------------------------------------------------------===// + +TEST_F(QuantumStateTest, printRendersImaginaryAmplitudes) { + auto qs = QuantumState::singletonZero(q[0], 4); + ASSERT_TRUE(qs.applyMatrix1Q(q[0], q[0], hOp.getUnitaryMatrix()).succeeded()); + ASSERT_TRUE( + qs.applyMatrix1Q(q[0], q[0], SOp::getUnitaryMatrix()).succeeded()); + EXPECT_NE(printed(qs).find(" i"), std::string::npos); +} + +} // namespace diff --git a/mlir/unittests/Dialect/QCO/Transforms/Optimizations/ConstantPropagation/test_unionTable.cpp b/mlir/unittests/Dialect/QCO/Transforms/Optimizations/ConstantPropagation/test_unionTable.cpp new file mode 100644 index 0000000000..94eac0c5fe --- /dev/null +++ b/mlir/unittests/Dialect/QCO/Transforms/Optimizations/ConstantPropagation/test_unionTable.cpp @@ -0,0 +1,461 @@ +/* + * Copyright (c) 2023 - 2026 Chair for Design Automation, TUM + * Copyright (c) 2025 - 2026 Munich Quantum Software Company GmbH + * All rights reserved. + * + * SPDX-License-Identifier: MIT + * + * Licensed under the MIT License + */ + +#include "ConstantPropagation/UnionTable.hpp" +#include "mlir/Dialect/QCO/Builder/QCOProgramBuilder.h" +#include "mlir/Dialect/QCO/IR/QCODialect.h" +#include "mlir/Dialect/QCO/IR/QCOOps.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +using namespace mlir; +using namespace mlir::qco; + +static std::string printed(const UnionTable& ut) { + std::string s; + llvm::raw_string_ostream os(s); + ut.print(os); + return s; +} + +namespace { + +class UnionTableTest : public testing::Test { +protected: + MLIRContext context; + QCOProgramBuilder builder; + + std::array q{}; + HOp hOp; + XOp xOp; + DCXOp dcxOp; + + UnionTableTest() : builder(&context) {} + + void SetUp() override { + DialectRegistry registry; + registry.insert(); + context.appendDialectRegistry(registry); + context.loadAllAvailableDialects(); + + builder.initialize(); + auto reg = builder.allocQubitRegister(8); + for (size_t i = 0; i < q.size(); ++i) { + q[i] = reg[i]; + } + const auto qt = q[0].getType(); + hOp = HOp::create(builder, builder.getLoc(), qt, q[0]); + xOp = XOp::create(builder, builder.getLoc(), qt, q[0]); + dcxOp = DCXOp::create(builder, builder.getLoc(), qt, qt, q[0], q[1]); + } + + static UnionTable make(size_t maxAmplitudes = 16, + size_t maxHybridStates = 8) { + return {maxAmplitudes, maxHybridStates}; + } +}; + +//===----------------------------------------------------------------------===// +// Seeding +//===----------------------------------------------------------------------===// + +TEST_F(UnionTableTest, seedQubitStartsInZero) { + auto ut = make(); + ut.seedQubit(q[0]); + EXPECT_TRUE(ut.isTracked(q[0])); + EXPECT_TRUE(ut.isQubitAlwaysZero(q[0])); + EXPECT_FALSE(ut.isQubitAlwaysOne(q[0])); + EXPECT_FALSE(ut.areStatesAllTop()); +} + +TEST_F(UnionTableTest, seedQubitCantBeCalledTwice) { + auto ut = make(); + ut.seedQubit(q[0]); + ASSERT_TRUE(ut.applyMatrix1Q(q[0], q[0], xOp.getUnitaryMatrix()).succeeded()); + ut.seedQubit(q[0]); // must not reset the qubit back to |0> + EXPECT_TRUE(ut.isQubitAlwaysOne(q[0])); +} + +TEST_F(UnionTableTest, seedClassicalRecordsConstant) { + auto ut = make(); + Value c = builder.boolConstant(true); + ut.seedClassical(c, builder.getBoolAttr(true)); + EXPECT_TRUE(ut.isTracked(c)); + EXPECT_TRUE(ut.isClassicalAlwaysTrue(c)); + EXPECT_FALSE(ut.isClassicalAlwaysFalse(c)); +} + +TEST_F(UnionTableTest, untrackedValueQueriesAreFalse) { + const auto ut = make(); + EXPECT_FALSE(ut.isTracked(q[0])); + EXPECT_FALSE(ut.isQubitAlwaysZero(q[0])); + EXPECT_FALSE(ut.isClassicalAlwaysTrue(q[0])); +} + +//===----------------------------------------------------------------------===// +// Factorisation +//===----------------------------------------------------------------------===// + +TEST_F(UnionTableTest, independentQubitsStayFactored) { + auto ut = make(); + ut.seedQubit(q[0]); + ut.seedQubit(q[1]); + ASSERT_TRUE(ut.applyMatrix1Q(q[0], q[0], xOp.getUnitaryMatrix()).succeeded()); + EXPECT_TRUE(ut.isQubitAlwaysOne(q[0])); + EXPECT_TRUE(ut.isQubitAlwaysZero(q[1])); + // Two independent factors print on two lines (a coalesced pair would be one). + EXPECT_NE(printed(ut).find('\n'), std::string::npos); +} + +TEST_F(UnionTableTest, twoQubitGateMergeTargets) { + auto ut = make(); + ut.seedQubit(q[0]); + ut.seedQubit(q[1]); + ASSERT_TRUE(ut.applyMatrix1Q(q[0], q[0], xOp.getUnitaryMatrix()).succeeded()); + ASSERT_TRUE(ut.applyMatrix2Q(q[0], q[1], q[0], q[1], dcxOp.getUnitaryMatrix()) + .succeeded()); + EXPECT_TRUE(ut.isQubitAlwaysZero(q[0])); + EXPECT_TRUE(ut.isQubitAlwaysOne(q[1])); +} + +TEST_F(UnionTableTest, controlledGateFiresAcrossSlots) { + auto ut = make(); + ut.seedQubit(q[0]); + ut.seedQubit(q[1]); + ASSERT_TRUE(ut.applyMatrix1Q(q[0], q[0], xOp.getUnitaryMatrix()).succeeded()); + ASSERT_TRUE( + ut.applyMatrix1Q(q[1], q[1], xOp.getUnitaryMatrix(), {q[0]}, {q[0]}) + .succeeded()); + EXPECT_TRUE(ut.isQubitAlwaysOne(q[1])); +} + +TEST_F(UnionTableTest, controlledGateDoesNotFireWhenControlIsZero) { + auto ut = make(); + ut.seedQubit(q[0]); + ut.seedQubit(q[1]); + ASSERT_TRUE( + ut.applyMatrix1Q(q[1], q[1], xOp.getUnitaryMatrix(), {q[0]}, {q[0]}) + .succeeded()); + EXPECT_TRUE(ut.isQubitAlwaysZero(q[1])); +} + +TEST_F(UnionTableTest, applyToUnseededQubitFails) { + auto ut = make(); + ut.seedQubit(q[0]); + EXPECT_TRUE(ut.applyMatrix1Q(q[1], q[1], xOp.getUnitaryMatrix()).failed()); +} + +TEST_F(UnionTableTest, nonEmptyControlInEmptyControlOutFails) { + auto ut = make(); + ut.seedQubit(q[0]); + ut.seedQubit(q[1]); + EXPECT_TRUE(ut.applyMatrix1Q(q[1], q[1], xOp.getUnitaryMatrix(), {q[0]}, {}) + .failed()); +} + +//===----------------------------------------------------------------------===// +// Classical controls +//===----------------------------------------------------------------------===// + +TEST_F(UnionTableTest, classicalControlSkipsGate) { + auto ut = make(); + ut.seedQubit(q[0]); + Value c = builder.boolConstant(false); + ut.seedClassical(c, builder.getBoolAttr(false)); + ASSERT_TRUE(ut.applyMatrix1Q(q[0], q[0], xOp.getUnitaryMatrix(), {}, {}, {c}) + .succeeded()); + EXPECT_TRUE(ut.isQubitAlwaysZero(q[0])); +} + +TEST_F(UnionTableTest, unresolvedClassicalControlFails) { + auto ut = make(); + ut.seedQubit(q[0]); + Value c = builder.boolConstant(false); + EXPECT_TRUE(ut.applyMatrix1Q(q[0], q[0], xOp.getUnitaryMatrix(), {}, {}, {c}) + .failed()); +} + +//===----------------------------------------------------------------------===// +// Measurement / reset +//===----------------------------------------------------------------------===// + +TEST_F(UnionTableTest, measureDeterministicRecordsBit) { + auto ut = make(); + ut.seedQubit(q[0]); + Value result = builder.boolConstant(false); + ASSERT_TRUE(ut.applyMatrix1Q(q[0], q[0], xOp.getUnitaryMatrix()).succeeded()); + ASSERT_TRUE(ut.measureQubit(q[0], q[0], result).succeeded()); + EXPECT_TRUE(ut.isClassicalAlwaysTrue(result)); +} + +TEST_F(UnionTableTest, measureSuperpositionTopsTheState) { + auto ut = make(); + ut.seedQubit(q[0]); + Value result = builder.boolConstant(false); + ASSERT_TRUE(ut.applyMatrix1Q(q[0], q[0], hOp.getUnitaryMatrix()).succeeded()); + ASSERT_TRUE(ut.measureQubit(q[0], q[0], result).succeeded()); + EXPECT_TRUE(ut.areStatesAllTop()); + EXPECT_FALSE(ut.isClassicalAlwaysTrue(result)); +} + +TEST_F(UnionTableTest, resetForcesZero) { + auto ut = make(); + ut.seedQubit(q[0]); + ASSERT_TRUE(ut.applyMatrix1Q(q[0], q[0], xOp.getUnitaryMatrix()).succeeded()); + ASSERT_TRUE(ut.resetQubit(q[0], q[0]).succeeded()); + EXPECT_TRUE(ut.isQubitAlwaysZero(q[0])); +} + +//===----------------------------------------------------------------------===// +// Global phase +//===----------------------------------------------------------------------===// + +TEST_F(UnionTableTest, globalPhaseIsRecordedOnce) { + auto ut = make(); + ut.seedQubit(q[0]); + Value theta = builder.floatConstant(std::numbers::pi); + ut.seedClassical(theta, builder.getF64FloatAttr(std::numbers::pi)); + ASSERT_TRUE(ut.addGlobalPhase(theta).succeeded()); + EXPECT_NE(printed(ut).find("phase="), std::string::npos); +} + +TEST_F(UnionTableTest, propagateClassicalFoldsAcrossSlots) { + auto ut = make(); + Value lhs = builder.intConstant(2); + Value rhs = builder.intConstant(5); + ut.seedClassical(lhs, builder.getIntegerAttr(lhs.getType(), 2)); + ut.seedClassical(rhs, builder.getIntegerAttr(rhs.getType(), 5)); + auto add = arith::AddIOp::create(builder, builder.getLoc(), lhs, rhs); + ut.propagateClassical(add.getOperation()); + EXPECT_TRUE(ut.isTracked(add.getResult())); + EXPECT_FALSE(ut.isClassicalAlwaysFalse(add.getResult())); + EXPECT_NE(printed(ut).find('7'), std::string::npos); +} + +//===----------------------------------------------------------------------===// +// Control analysis +//===----------------------------------------------------------------------===// + +TEST_F(UnionTableTest, controlsSatisfiableWhenBothCanBeOne) { + auto ut = make(); + ut.seedQubit(q[0]); + ut.seedQubit(q[1]); + ASSERT_TRUE(ut.applyMatrix1Q(q[0], q[0], xOp.getUnitaryMatrix()).succeeded()); + ASSERT_TRUE(ut.applyMatrix1Q(q[1], q[1], xOp.getUnitaryMatrix()).succeeded()); + EXPECT_TRUE(ut.areControlsSatisfiable({q[0], q[1]})); +} + +TEST_F(UnionTableTest, controlsUnsatisfiableWhenAQubitIsAlwaysZero) { + auto ut = make(); + ut.seedQubit(q[0]); + ut.seedQubit(q[1]); + ASSERT_TRUE(ut.applyMatrix1Q(q[0], q[0], xOp.getUnitaryMatrix()).succeeded()); + EXPECT_FALSE(ut.areControlsSatisfiable({q[0], q[1]})); +} + +TEST_F(UnionTableTest, negativeClassicalControlSatisfiedByFalseConstant) { + auto ut = make(); + Value c = builder.boolConstant(false); + ut.seedClassical(c, builder.getBoolAttr(false)); + EXPECT_FALSE(ut.areControlsSatisfiable({}, {c})); + EXPECT_TRUE(ut.areControlsSatisfiable({}, {}, {c})); +} + +TEST_F(UnionTableTest, superfluousControlsListsAlwaysOneQubit) { + auto ut = make(); + ut.seedQubit(q[0]); + ASSERT_TRUE(ut.applyMatrix1Q(q[0], q[0], xOp.getUnitaryMatrix()).succeeded()); + const auto result = ut.getSuperfluousControls({q[0]}); + EXPECT_FALSE(result.completelySuperfluous); + EXPECT_TRUE(result.superfluousQubits.contains(q[0])); +} + +TEST_F(UnionTableTest, superfluousControlsFlagsDeadGate) { + auto ut = make(); + ut.seedQubit(q[0]); + const auto result = ut.getSuperfluousControls({q[0]}); + EXPECT_TRUE(result.completelySuperfluous); +} + +//===----------------------------------------------------------------------===// +// markQubitsTop / forwarding +//===----------------------------------------------------------------------===// + +TEST_F(UnionTableTest, markQubitsTopClearsQuantumInfo) { + auto ut = make(); + ut.seedQubit(q[0]); + ut.markQubitsTop({q[0]}); + EXPECT_TRUE(ut.areStatesAllTop()); + EXPECT_FALSE(ut.isQubitAlwaysZero(q[0])); +} + +TEST_F(UnionTableTest, forwardValueRenamesQubit) { + auto ut = make(); + ut.seedQubit(q[0]); + ASSERT_TRUE(ut.applyMatrix1Q(q[0], q[0], xOp.getUnitaryMatrix()).succeeded()); + ut.forwardValue(q[0], q[1]); + EXPECT_FALSE(ut.isTracked(q[0])); + EXPECT_TRUE(ut.isQubitAlwaysOne(q[1])); +} + +//===----------------------------------------------------------------------===// +// join +//===----------------------------------------------------------------------===// + +TEST_F(UnionTableTest, joinOfAgreeingBranchesKeepsTheFact) { + auto a = make(); + a.seedQubit(q[0]); + ASSERT_TRUE(a.applyMatrix1Q(q[0], q[0], xOp.getUnitaryMatrix()).succeeded()); + auto b = make(); + b.seedQubit(q[0]); + ASSERT_TRUE(b.applyMatrix1Q(q[0], q[0], xOp.getUnitaryMatrix()).succeeded()); + + a.join(b); + EXPECT_TRUE(a.isQubitAlwaysOne(q[0])); + EXPECT_FALSE(a.isAllTop()); +} + +TEST_F(UnionTableTest, joinOfDisagreeingBranchesIsAProbabilisticSplit) { + auto a = make(); + a.seedQubit(q[0]); + auto b = make(); + b.seedQubit(q[0]); + ASSERT_TRUE(b.applyMatrix1Q(q[0], q[0], xOp.getUnitaryMatrix()).succeeded()); + + a.join(b); + EXPECT_FALSE(a.isQubitAlwaysZero(q[0])); + EXPECT_FALSE(a.isQubitAlwaysOne(q[0])); + EXPECT_FALSE(a.isAllTop()); + EXPECT_NE(printed(a).find("p=0.5000"), std::string::npos); +} + +TEST_F(UnionTableTest, joinOfDifferentEntanglementStructureTops) { + auto a = make(); + a.seedQubit(q[0]); + a.seedQubit(q[1]); + ASSERT_TRUE(a.applyMatrix2Q(q[0], q[1], q[0], q[1], dcxOp.getUnitaryMatrix()) + .succeeded()); + auto b = make(); + b.seedQubit(q[0]); + b.seedQubit(q[1]); + + a.join(b); + EXPECT_TRUE(a.isAllTop()); +} + +TEST_F(UnionTableTest, joinKeepsClassicalFactOnlyWhenShared) { + Value c = builder.boolConstant(true); + + auto a = make(); + a.seedClassical(c, builder.getBoolAttr(true)); + auto agree = make(); + agree.seedClassical(c, builder.getBoolAttr(true)); + a.join(agree); + EXPECT_TRUE(a.isClassicalAlwaysTrue(c)); + + auto d = make(); + d.seedClassical(c, builder.getBoolAttr(true)); + auto disagree = make(); + disagree.seedClassical(c, builder.getBoolAttr(false)); + d.join(disagree); + EXPECT_FALSE(d.isClassicalAlwaysTrue(c)); + EXPECT_FALSE(d.isClassicalAlwaysFalse(c)); +} + +TEST_F(UnionTableTest, joinOverflowingAFactorTopsOnlyThatFactor) { + auto a = make(16, 2); + a.seedQubit(q[0]); + a.seedQubit(q[1]); + auto b = make(16, 2); + b.seedQubit(q[0]); + b.seedQubit(q[1]); + ASSERT_TRUE(b.applyMatrix1Q(q[0], q[0], xOp.getUnitaryMatrix()).succeeded()); + a.join(b); + ASSERT_FALSE(a.isAllTop()); + + auto c = make(16, 2); + c.seedQubit(q[0]); + c.seedQubit(q[1]); + ASSERT_TRUE(c.applyMatrix1Q(q[0], q[0], hOp.getUnitaryMatrix()).succeeded()); + a.join(c); + + EXPECT_FALSE(a.isAllTop()); + EXPECT_FALSE(a.isQubitAlwaysZero(q[0])); // {q0} factor collapsed to top + EXPECT_TRUE(a.isQubitAlwaysZero(q[1])); // {q1} factor reconciled normally +} + +//===----------------------------------------------------------------------===// +// Equality +//===----------------------------------------------------------------------===// + +TEST_F(UnionTableTest, equalityIsOrderIndependent) { + auto a = make(); + a.seedQubit(q[0]); + a.seedQubit(q[1]); + auto b = make(); + b.seedQubit(q[1]); + b.seedQubit(q[0]); + EXPECT_TRUE(a == b); +} + +TEST_F(UnionTableTest, equalitySeesAppliedGates) { + auto a = make(); + a.seedQubit(q[0]); + auto b = make(); + b.seedQubit(q[0]); + ASSERT_TRUE(b.applyMatrix1Q(q[0], q[0], xOp.getUnitaryMatrix()).succeeded()); + EXPECT_FALSE(a == b); +} + +TEST_F(UnionTableTest, markAllTopIsAbsorbing) { + auto ut = make(); + ut.seedQubit(q[0]); + ut.markAllTop(); + EXPECT_TRUE(ut.isAllTop()); + EXPECT_TRUE(ut.areStatesAllTop()); + EXPECT_EQ(printed(ut), ""); + ut.seedQubit(q[1]); + EXPECT_FALSE(ut.isTracked(q[1])); +} + +TEST_F(UnionTableTest, superfluousControlsListsAlwaysTrueClassicalControl) { + auto ut = make(); + Value c = builder.boolConstant(true); + ut.seedClassical(c, builder.getBoolAttr(true)); + + const auto result = ut.getSuperfluousControls({}, {c}); + EXPECT_FALSE(result.completelySuperfluous); + EXPECT_TRUE(result.superfluousClassicalValues.contains(c)); +} + +TEST_F(UnionTableTest, + superfluousControlsListsAlwaysFalseNegativeClassicalControl) { + auto ut = make(); + Value c = builder.boolConstant(false); + ut.seedClassical(c, builder.getBoolAttr(false)); + + const auto result = ut.getSuperfluousControls({}, {}, {c}); + EXPECT_FALSE(result.completelySuperfluous); + EXPECT_TRUE(result.superfluousClassicalValues.contains(c)); +} + +} // namespace diff --git a/mlir/unittests/Dialect/QCO/Transforms/Optimizations/test_qco_constant_propagation.cpp b/mlir/unittests/Dialect/QCO/Transforms/Optimizations/test_qco_constant_propagation.cpp new file mode 100644 index 0000000000..32ea41a0e9 --- /dev/null +++ b/mlir/unittests/Dialect/QCO/Transforms/Optimizations/test_qco_constant_propagation.cpp @@ -0,0 +1,260 @@ +/* + * Copyright (c) 2023 - 2026 Chair for Design Automation, TUM + * Copyright (c) 2025 - 2026 Munich Quantum Software Company GmbH + * All rights reserved. + * + * SPDX-License-Identifier: MIT + * + * Licensed under the MIT License + */ + +#include "mlir/Dialect/MQT/IR/MQTDialect.h" +#include "mlir/Dialect/QCO/Builder/QCOProgramBuilder.h" +#include "mlir/Dialect/QCO/IR/QCODialect.h" +#include "mlir/Dialect/QCO/IR/QCOOps.h" +#include "mlir/Dialect/QCO/Transforms/Passes.h" +#include "mlir/Dialect/QTensor/IR/QTensorDialect.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +using namespace mlir; +using namespace mlir::qco; + +/// Number of ops of a given kind anywhere in the module (bodies included). +template static unsigned countOps(ModuleOp module) { + unsigned n = 0; + module.walk([&](OpT) { ++n; }); + return n; +} + +/// The first op of a given kind in walk order, or a null handle if there is +/// none. +template static OpT firstOp(ModuleOp module) { + OpT found; + module.walk([&](OpT op) { + found = op; + return WalkResult::interrupt(); + }); + return found; +} + +namespace { + +class ConstantPropagationTest : public testing::Test { +protected: + MLIRContext context; + QCOProgramBuilder builder; + + ConstantPropagationTest() : builder(&context) {} + + void SetUp() override { + DialectRegistry registry; + registry.insert(); + context.appendDialectRegistry(registry); + context.loadAllAvailableDialects(); + builder.initialize(); + } + + static LogicalResult run(ModuleOp module, std::size_t maxAmplitudes = 4, + std::size_t maxHybridStates = 4) { + PassManager pm(module.getContext()); + pm.addPass(createConstantPropagation( + ConstantPropagationOptions{.maximumNonzeroAmplitudes = maxAmplitudes, + .maximumHybridStates = maxHybridStates})); + return pm.run(module); + } +}; + +TEST_F(ConstantPropagationTest, dropsGateWithUnsatisfiableControl) { + auto reg = builder.allocQubitRegister(2); + builder.cx(reg[0], reg[1]); + const auto module = builder.finalize(); + + ASSERT_TRUE(succeeded(run(*module))); + EXPECT_TRUE(succeeded(verify(*module))); + EXPECT_EQ(countOps(*module), 0U); +} + +TEST_F(ConstantPropagationTest, stripsAlwaysSatisfiedControl) { + auto reg = builder.allocQubitRegister(3); + Value one = builder.x(reg[0]); + Value sup = builder.h(reg[2]); + const SmallVector controls{one, sup}; + builder.mcx(controls, reg[1]); + const auto module = builder.finalize(); + + ASSERT_TRUE(succeeded(run(*module))); + EXPECT_TRUE(succeeded(verify(*module))); + + ASSERT_EQ(countOps(*module), 1U); + EXPECT_EQ(firstOp(*module).getNumControls(), 1U); +} + +TEST_F(ConstantPropagationTest, unwrapsGateWhenEveryControlRedundant) { + auto reg = builder.allocQubitRegister(2); + Value one = builder.x(reg[0]); + builder.cx(one, reg[1]); + const auto module = builder.finalize(); + + ASSERT_TRUE(succeeded(run(*module))); + EXPECT_TRUE(succeeded(verify(*module))); + + EXPECT_EQ(countOps(*module), 0U); + EXPECT_EQ(countOps(*module), 2U); +} + +TEST_F(ConstantPropagationTest, leavesGateAloneWhenStateIsImprecise) { + auto reg = builder.allocQubitRegister(2); + Value sup = builder.h(reg[0]); + builder.cx(sup, reg[1]); + const auto module = builder.finalize(); + + ASSERT_TRUE(succeeded(run(*module, 1))); + EXPECT_TRUE(succeeded(verify(*module))); + EXPECT_EQ(countOps(*module), 1U); +} + +TEST_F(ConstantPropagationTest, dropsGateWhenOneOfSeveralControlsIsAlwaysZero) { + auto reg = builder.allocQubitRegister(3); + Value one = builder.x(reg[0]); + const SmallVector controls{one, reg[1]}; + builder.mcx(controls, reg[2]); + const auto module = builder.finalize(); + + ASSERT_TRUE(succeeded(run(*module))); + EXPECT_TRUE(succeeded(verify(*module))); + EXPECT_EQ(countOps(*module), 0U); + EXPECT_EQ(countOps(*module), 1U); +} + +TEST_F(ConstantPropagationTest, stripsTrailingAlwaysOneControl) { + auto reg = builder.allocQubitRegister(3); + Value sup = builder.h(reg[0]); + Value one = builder.x(reg[1]); + const SmallVector controls{sup, one}; + builder.mcx(controls, reg[2]); + const auto module = builder.finalize(); + + ASSERT_TRUE(succeeded(run(*module))); + EXPECT_TRUE(succeeded(verify(*module))); + + ASSERT_EQ(countOps(*module), 1U); + auto ctrl = firstOp(*module); + EXPECT_EQ(ctrl.getNumControls(), 1U); + EXPECT_TRUE(ctrl.getInputControl(0) == sup); + EXPECT_EQ(countOps(*module), 2U); +} + +TEST_F(ConstantPropagationTest, stripsAllButOneControl) { + auto reg = builder.allocQubitRegister(4); + Value a = builder.x(reg[0]); + Value b = builder.x(reg[1]); + Value sup = builder.h(reg[3]); + const SmallVector controls{a, b, sup}; + builder.mcx(controls, reg[2]); + const auto module = builder.finalize(); + + ASSERT_TRUE(succeeded(run(*module))); + EXPECT_TRUE(succeeded(verify(*module))); + + ASSERT_EQ(countOps(*module), 1U); + auto ctrl = firstOp(*module); + EXPECT_EQ(ctrl.getNumControls(), 1U); + EXPECT_TRUE(ctrl.getInputControl(0) == sup); +} + +TEST_F(ConstantPropagationTest, + unwrapsMultiControlGateWhenAllControlsRedundant) { + auto reg = builder.allocQubitRegister(3); + Value a = builder.x(reg[0]); + Value b = builder.x(reg[1]); + const SmallVector controls{a, b}; + builder.mcx(controls, reg[2]); + const auto module = builder.finalize(); + + ASSERT_TRUE(succeeded(run(*module))); + EXPECT_TRUE(succeeded(verify(*module))); + + EXPECT_EQ(countOps(*module), 0U); + EXPECT_EQ(countOps(*module), 3U); +} + +TEST_F(ConstantPropagationTest, simplifiesChainOfControlledGates) { + auto reg = builder.allocQubitRegister(3); + Value q0 = builder.x(reg[0]); + Value q1 = builder.cx(q0, reg[1]).second; + builder.cx(q1, reg[2]); + const auto module = builder.finalize(); + + ASSERT_TRUE(succeeded(run(*module))); + EXPECT_TRUE(succeeded(verify(*module))); + + EXPECT_EQ(countOps(*module), 0U); + EXPECT_EQ(countOps(*module), 3U); +} + +TEST_F(ConstantPropagationTest, noControlledGatesIsNoOp) { + auto reg = builder.allocQubitRegister(2); + builder.x(reg[0]); + builder.h(reg[1]); + const auto module = builder.finalize(); + + ASSERT_TRUE(succeeded(run(*module))); + EXPECT_TRUE(succeeded(verify(*module))); + EXPECT_EQ(countOps(*module), 1U); + EXPECT_EQ(countOps(*module), 1U); +} + +TEST_F(ConstantPropagationTest, missingEntryPointIsANoOp) { + auto reg = builder.allocQubitRegister(2); + Value q0 = builder.x(reg[0]); + builder.cx(q0, reg[1]); + auto module = builder.finalize(); + + mqt::removeEntryPoint(mqt::getEntryPoint(*module).getOperation()); + + ASSERT_TRUE(succeeded(run(*module))); + EXPECT_EQ(countOps(*module), 1U); +} + +TEST_F(ConstantPropagationTest, multipleEntryPointsFail) { + auto reg = builder.allocQubitRegister(1); + builder.x(reg[0]); + auto module = builder.finalize(); + + OpBuilder ob(module->getContext()); + ob.setInsertionPointToEnd(module->getBody()); + auto second = func::FuncOp::create(ob, module->getLoc(), "second", + ob.getFunctionType({}, {})); + mqt::setEntryPoint(second.getOperation()); + + EXPECT_TRUE(failed(run(*module))); +} + +TEST_F(ConstantPropagationTest, runsOnProgramWithConstantIf) { + auto reg = builder.allocQubitRegister(1); + builder.qcoIf( + true, reg[0], [&](Value arg) { return builder.x(arg); }, + [&](Value arg) { return builder.h(arg); }); + const auto module = builder.finalize(); + + ASSERT_TRUE(succeeded(run(*module))); + EXPECT_TRUE(succeeded(verify(*module))); +} + +} // namespace