Skip to content

Commit 8e493cf

Browse files
authored
qdt.QIntSignMag (#1836)
"Sign magnitude" signed integers. @ncrubin wrote this
1 parent 69eff51 commit 8e493cf

File tree

4 files changed

+220
-0
lines changed

4 files changed

+220
-0
lines changed

qualtran/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,8 @@
6464
CInt,
6565
QIntOnesComp,
6666
CIntOnesComp,
67+
QIntSignMag,
68+
CIntSignMag,
6769
QUInt,
6870
CUInt,
6971
BQUInt,

qualtran/dtype/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@
2727

2828
from ._int_ones_complement import QIntOnesComp, CIntOnesComp
2929

30+
from ._int_signmag import QIntSignMag, CIntSignMag
31+
3032
from ._buint import BQUInt, BCUInt
3133

3234
from ._fxp import QFxp, CFxp

qualtran/dtype/_int_signmag.py

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
# Copyright 2026 Google LLC
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# https://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
from functools import cached_property
16+
from typing import Iterable, List, Sequence
17+
18+
import attrs
19+
import numpy as np
20+
from numpy.typing import NDArray
21+
22+
from qualtran.symbolics import is_symbolic, SymbolicInt
23+
24+
from ._base import BitEncoding, CDType, QDType
25+
26+
27+
@attrs.frozen
28+
class _IntSignMag(BitEncoding[int]):
29+
"""Sign-magnitude encoding.
30+
31+
The most significant bit is the sign bit (0=positive, 1=negative).
32+
The remaining bits encode the absolute value.
33+
"""
34+
35+
bitsize: SymbolicInt
36+
37+
def get_domain(self) -> Iterable[int]:
38+
max_val = 1 << (self.bitsize - 1)
39+
return range(-max_val + 1, max_val)
40+
41+
def to_bits(self, x: int) -> List[int]:
42+
if is_symbolic(self.bitsize):
43+
raise ValueError(f"cannot compute bits with symbolic {self.bitsize=}")
44+
self.assert_valid_val(x)
45+
return [1 if x < 0 else 0] + [
46+
int(b) for b in np.binary_repr(np.abs(x), width=self.bitsize - 1)
47+
]
48+
49+
def from_bits(self, bits: Sequence[int]) -> int:
50+
sign = bits[0]
51+
if self.bitsize == 1:
52+
return 0
53+
magnitude = 0
54+
for b in bits[1:]:
55+
magnitude = (magnitude << 1) | b
56+
return -magnitude if sign else magnitude
57+
58+
def assert_valid_val(self, val: int, debug_str: str = 'val'):
59+
if not isinstance(val, (int, np.integer)):
60+
raise ValueError(f"{debug_str} should be an integer, not {val!r}")
61+
max_val = 1 << (self.bitsize - 1)
62+
if val <= -max_val:
63+
raise ValueError(f"Too-small classical {self}: {val} encountered in {debug_str}")
64+
if val >= max_val:
65+
raise ValueError(f"Too-large classical {self}: {val} encountered in {debug_str}")
66+
67+
def assert_valid_val_array(self, val_array: NDArray[np.integer], debug_str: str = 'val'):
68+
max_val = 1 << (self.bitsize - 1)
69+
if np.any(val_array <= -max_val):
70+
raise ValueError(f"Too-small classical {self}s encountered in {debug_str}")
71+
if np.any(val_array >= max_val):
72+
raise ValueError(f"Too-large classical {self}s encountered in {debug_str}")
73+
74+
75+
@attrs.frozen
76+
class QIntSignMag(QDType[int]):
77+
"""Sign-magnitude signed quantum integer.
78+
79+
The most significant bit is the sign bit (0=positive, 1=negative),
80+
and the remaining bits encode the absolute value.
81+
82+
For an n-bit QSignInt, the representable range is [-(2^(n-1)-1), 2^(n-1)-1].
83+
Note that this means +0 and -0 are distinct representations.
84+
85+
Args:
86+
bitsize: The number of qubits used to represent the integer.
87+
"""
88+
89+
bitsize: SymbolicInt
90+
91+
def __attrs_post_init__(self):
92+
if isinstance(self.bitsize, int):
93+
if self.bitsize < 2:
94+
raise ValueError("bitsize must be >= 2.")
95+
96+
@cached_property
97+
def _bit_encoding(self) -> BitEncoding[int]:
98+
return _IntSignMag(self.bitsize)
99+
100+
def is_symbolic(self) -> bool:
101+
return is_symbolic(self.bitsize)
102+
103+
def __str__(self):
104+
return f'QIntSignMag({self.bitsize})'
105+
106+
107+
@attrs.frozen
108+
class CIntSignMag(CDType[int]):
109+
"""Sign-magnitude signed classical integer.
110+
111+
The most significant bit is the sign bit (0=positive, 1=negative),
112+
and the remaining bits encode the absolute value.
113+
114+
Args:
115+
bitsize: The number of qubits used to represent the integer.
116+
"""
117+
118+
bitsize: SymbolicInt
119+
120+
def __attrs_post_init__(self):
121+
if isinstance(self.bitsize, int):
122+
if self.bitsize < 2:
123+
raise ValueError("bitsize must be >= 2.")
124+
125+
@cached_property
126+
def _bit_encoding(self) -> BitEncoding[int]:
127+
return _IntSignMag(self.bitsize)
128+
129+
def is_symbolic(self) -> bool:
130+
return is_symbolic(self.bitsize)
131+
132+
def __str__(self):
133+
return f'CIntSignMag({self.bitsize})'
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
# Copyright 2026 Google LLC
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# https://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
16+
import pytest
17+
18+
from qualtran.dtype import QIntSignMag
19+
20+
21+
def test_basic_properties():
22+
dtype = QIntSignMag(4)
23+
assert dtype.num_qubits == 4
24+
assert not dtype.is_symbolic()
25+
assert str(dtype) == 'QIntSignMag(4)'
26+
27+
28+
def test_bitsize_validation():
29+
with pytest.raises(ValueError, match="bitsize must be >= 2"):
30+
QIntSignMag(1)
31+
32+
33+
def test_to_bits_positive():
34+
dtype = QIntSignMag(4)
35+
assert dtype.to_bits(5) == [0, 1, 0, 1]
36+
assert dtype.to_bits(0) == [0, 0, 0, 0]
37+
assert dtype.to_bits(7) == [0, 1, 1, 1]
38+
39+
40+
def test_to_bits_negative():
41+
dtype = QIntSignMag(4)
42+
assert dtype.to_bits(-5) == [1, 1, 0, 1]
43+
assert dtype.to_bits(-1) == [1, 0, 0, 1]
44+
assert dtype.to_bits(-7) == [1, 1, 1, 1]
45+
46+
47+
def test_from_bits():
48+
dtype = QIntSignMag(4)
49+
assert dtype.from_bits([0, 1, 0, 1]) == 5
50+
assert dtype.from_bits([1, 1, 0, 1]) == -5
51+
assert dtype.from_bits([0, 0, 0, 0]) == 0
52+
assert dtype.from_bits([1, 0, 0, 0]) == 0 # -0 == 0
53+
54+
55+
def test_roundtrip():
56+
# to_bits -> from_bits should be identity for all valid values.
57+
dtype = QIntSignMag(4)
58+
for val in dtype.get_classical_domain():
59+
bits = dtype.to_bits(val)
60+
recovered = dtype.from_bits(bits)
61+
assert recovered == val, f"Failed for {val}: bits={bits}, recovered={recovered}"
62+
63+
64+
def test_classical_domain():
65+
dtype = QIntSignMag(4)
66+
domain = list(dtype.get_classical_domain())
67+
# 4-bit sign-magnitude: range is [-7, 7]
68+
assert domain == list(range(-7, 8))
69+
70+
71+
def test_valid_classical_val():
72+
dtype = QIntSignMag(4)
73+
dtype.assert_valid_classical_val(0)
74+
dtype.assert_valid_classical_val(7)
75+
dtype.assert_valid_classical_val(-7)
76+
77+
78+
def test_invalid_classical_val():
79+
dtype = QIntSignMag(4)
80+
with pytest.raises(ValueError, match="Too-large"):
81+
dtype.assert_valid_classical_val(8)
82+
with pytest.raises(ValueError, match="Too-small"):
83+
dtype.assert_valid_classical_val(-8)

0 commit comments

Comments
 (0)