Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions qualtran/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,8 @@
CInt,
QIntOnesComp,
CIntOnesComp,
QIntSignMag,
CIntSignMag,
QUInt,
CUInt,
BQUInt,
Expand Down
2 changes: 2 additions & 0 deletions qualtran/dtype/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@

from ._int_ones_complement import QIntOnesComp, CIntOnesComp

from ._int_signmag import QIntSignMag, CIntSignMag

from ._buint import BQUInt, BCUInt

from ._fxp import QFxp, CFxp
Expand Down
133 changes: 133 additions & 0 deletions qualtran/dtype/_int_signmag.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
# Copyright 2026 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

from functools import cached_property
from typing import Iterable, List, Sequence

import attrs
import numpy as np
from numpy.typing import NDArray

from qualtran.symbolics import is_symbolic, SymbolicInt

from ._base import BitEncoding, CDType, QDType


@attrs.frozen
class _IntSignMag(BitEncoding[int]):
"""Sign-magnitude encoding.

The most significant bit is the sign bit (0=positive, 1=negative).
The remaining bits encode the absolute value.
"""

bitsize: SymbolicInt

def get_domain(self) -> Iterable[int]:
max_val = 1 << (self.bitsize - 1)
return range(-max_val + 1, max_val)

def to_bits(self, x: int) -> List[int]:
if is_symbolic(self.bitsize):
raise ValueError(f"cannot compute bits with symbolic {self.bitsize=}")
self.assert_valid_val(x)
return [1 if x < 0 else 0] + [
int(b) for b in np.binary_repr(np.abs(x), width=self.bitsize - 1)
]

def from_bits(self, bits: Sequence[int]) -> int:
sign = bits[0]
if self.bitsize == 1:
return 0
magnitude = 0
for b in bits[1:]:
magnitude = (magnitude << 1) | b
return -magnitude if sign else magnitude

def assert_valid_val(self, val: int, debug_str: str = 'val'):
if not isinstance(val, (int, np.integer)):
raise ValueError(f"{debug_str} should be an integer, not {val!r}")
max_val = 1 << (self.bitsize - 1)
if val <= -max_val:
raise ValueError(f"Too-small classical {self}: {val} encountered in {debug_str}")
if val >= max_val:
raise ValueError(f"Too-large classical {self}: {val} encountered in {debug_str}")

def assert_valid_val_array(self, val_array: NDArray[np.integer], debug_str: str = 'val'):
max_val = 1 << (self.bitsize - 1)
if np.any(val_array <= -max_val):
raise ValueError(f"Too-small classical {self}s encountered in {debug_str}")
if np.any(val_array >= max_val):
raise ValueError(f"Too-large classical {self}s encountered in {debug_str}")


@attrs.frozen
class QIntSignMag(QDType[int]):
"""Sign-magnitude signed quantum integer.

The most significant bit is the sign bit (0=positive, 1=negative),
and the remaining bits encode the absolute value.

For an n-bit QSignInt, the representable range is [-(2^(n-1)-1), 2^(n-1)-1].
Note that this means +0 and -0 are distinct representations.

Args:
bitsize: The number of qubits used to represent the integer.
"""

bitsize: SymbolicInt

def __attrs_post_init__(self):
if isinstance(self.bitsize, int):
if self.bitsize < 2:
raise ValueError("bitsize must be >= 2.")

@cached_property
def _bit_encoding(self) -> BitEncoding[int]:
return _IntSignMag(self.bitsize)

def is_symbolic(self) -> bool:
return is_symbolic(self.bitsize)

def __str__(self):
return f'QIntSignMag({self.bitsize})'


@attrs.frozen
class CIntSignMag(CDType[int]):
"""Sign-magnitude signed classical integer.

The most significant bit is the sign bit (0=positive, 1=negative),
and the remaining bits encode the absolute value.

Args:
bitsize: The number of qubits used to represent the integer.
"""

bitsize: SymbolicInt

def __attrs_post_init__(self):
if isinstance(self.bitsize, int):
if self.bitsize < 2:
raise ValueError("bitsize must be >= 2.")

@cached_property
def _bit_encoding(self) -> BitEncoding[int]:
return _IntSignMag(self.bitsize)

def is_symbolic(self) -> bool:
return is_symbolic(self.bitsize)

def __str__(self):
return f'CIntSignMag({self.bitsize})'
83 changes: 83 additions & 0 deletions qualtran/dtype/_int_signmag_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
# Copyright 2026 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.


import pytest

from qualtran.dtype import QIntSignMag


def test_basic_properties():
dtype = QIntSignMag(4)
assert dtype.num_qubits == 4
assert not dtype.is_symbolic()
assert str(dtype) == 'QIntSignMag(4)'


def test_bitsize_validation():
with pytest.raises(ValueError, match="bitsize must be >= 2"):
QIntSignMag(1)


def test_to_bits_positive():
dtype = QIntSignMag(4)
assert dtype.to_bits(5) == [0, 1, 0, 1]
assert dtype.to_bits(0) == [0, 0, 0, 0]
assert dtype.to_bits(7) == [0, 1, 1, 1]


def test_to_bits_negative():
dtype = QIntSignMag(4)
assert dtype.to_bits(-5) == [1, 1, 0, 1]
assert dtype.to_bits(-1) == [1, 0, 0, 1]
assert dtype.to_bits(-7) == [1, 1, 1, 1]


def test_from_bits():
dtype = QIntSignMag(4)
assert dtype.from_bits([0, 1, 0, 1]) == 5
assert dtype.from_bits([1, 1, 0, 1]) == -5
assert dtype.from_bits([0, 0, 0, 0]) == 0
assert dtype.from_bits([1, 0, 0, 0]) == 0 # -0 == 0


def test_roundtrip():
# to_bits -> from_bits should be identity for all valid values.
dtype = QIntSignMag(4)
for val in dtype.get_classical_domain():
bits = dtype.to_bits(val)
recovered = dtype.from_bits(bits)
assert recovered == val, f"Failed for {val}: bits={bits}, recovered={recovered}"


def test_classical_domain():
dtype = QIntSignMag(4)
domain = list(dtype.get_classical_domain())
# 4-bit sign-magnitude: range is [-7, 7]
assert domain == list(range(-7, 8))


def test_valid_classical_val():
dtype = QIntSignMag(4)
dtype.assert_valid_classical_val(0)
dtype.assert_valid_classical_val(7)
dtype.assert_valid_classical_val(-7)


def test_invalid_classical_val():
dtype = QIntSignMag(4)
with pytest.raises(ValueError, match="Too-large"):
dtype.assert_valid_classical_val(8)
with pytest.raises(ValueError, match="Too-small"):
dtype.assert_valid_classical_val(-8)
Loading