diff --git a/pyproject.toml b/pyproject.toml index b39d7f3..2c70722 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -35,6 +35,7 @@ classifiers = [ [project.optional-dependencies] dev = [ "mypy>=1.19.1", + "pytest>=9.0.2", "ruff>=0.14.10", "types-pyyaml>=6.0.12", ] diff --git a/tests/bitspack_test.py b/tests/bitspack_test.py new file mode 100644 index 0000000..4f7025f --- /dev/null +++ b/tests/bitspack_test.py @@ -0,0 +1,100 @@ +""" +Tests for qmp.utility.bitspack pack_int and unpack_int. +""" + +import torch + +from qmp.utility.bitspack import pack_int, unpack_int + + +class TestPackUnpackIntSize1: + """pack_int / unpack_int round-trip for 1-bit integers.""" + + def test_exact_multiple_of_elements_per_byte(self) -> None: + """16 elements pack into 2 bytes and unpack back without loss.""" + tensor = torch.tensor([1, 0, 1, 1, 0, 0, 1, 0, 1, 1, 0, 1, 1, 0, 0, 1], dtype=torch.uint8) + packed = pack_int(tensor, 1) + unpacked = unpack_int(packed, 1, tensor.shape[-1]) + assert torch.all(unpacked == tensor) + + def test_with_padding(self) -> None: + """15 elements require padding to 16; unpacking strips the pad and recovers the original.""" + tensor = torch.tensor([1, 0, 1, 1, 0, 0, 1, 0, 1, 1, 0, 1, 1, 0, 0], dtype=torch.uint8) + packed = pack_int(tensor, 1) + unpacked = unpack_int(packed, 1, tensor.shape[-1]) + assert torch.all(unpacked == tensor) + + def test_multi_dimensional(self) -> None: + """4×4 tensor packs each row independently and unpacks back correctly.""" + tensor = torch.tensor([[1, 0, 1, 1], [0, 0, 1, 0], [1, 1, 0, 1], [1, 0, 0, 1]], dtype=torch.uint8) + packed = pack_int(tensor, 1) + unpacked = unpack_int(packed, 1, tensor.shape[-1]) + assert torch.all(unpacked == tensor) + + def test_large_random_tensor(self) -> None: + """1000 random 1-bit values survive a pack/unpack round-trip.""" + tensor = torch.randint(0, 2, (1000,), dtype=torch.uint8) + packed = pack_int(tensor, 1) + unpacked = unpack_int(packed, 1, tensor.shape[-1]) + assert torch.all(unpacked == tensor) + + +class TestPackUnpackIntSize2: + """pack_int / unpack_int round-trip for 2-bit integers.""" + + def test_exact_multiple_of_elements_per_byte(self) -> None: + """16 elements (values 0–3) pack into 4 bytes and unpack back without loss.""" + tensor = torch.tensor([3, 2, 1, 0, 3, 2, 1, 0, 3, 2, 1, 0, 3, 2, 1, 0], dtype=torch.uint8) + packed = pack_int(tensor, 2) + unpacked = unpack_int(packed, 2, tensor.shape[-1]) + assert torch.all(unpacked == tensor) + + def test_with_padding(self) -> None: + """14 elements require padding to 16; unpacking strips the pad and recovers the original.""" + tensor = torch.tensor([3, 2, 1, 0, 3, 2, 1, 0, 3, 2, 1, 0, 3, 2], dtype=torch.uint8) + packed = pack_int(tensor, 2) + unpacked = unpack_int(packed, 2, tensor.shape[-1]) + assert torch.all(unpacked == tensor) + + def test_large_random_tensor(self) -> None: + """1000 random 2-bit values survive a pack/unpack round-trip.""" + tensor = torch.randint(0, 4, (1000,), dtype=torch.uint8) + packed = pack_int(tensor, 2) + unpacked = unpack_int(packed, 2, tensor.shape[-1]) + assert torch.all(unpacked == tensor) + + +class TestPackUnpackIntSize4: + """pack_int / unpack_int round-trip for 4-bit integers.""" + + def test_exact_multiple_of_elements_per_byte(self) -> None: + """16 elements (values 0–15) pack into 8 bytes and unpack back without loss.""" + tensor = torch.tensor([15, 10, 5, 0, 15, 10, 5, 0, 15, 10, 5, 0, 15, 10, 5, 0], dtype=torch.uint8) + packed = pack_int(tensor, 4) + unpacked = unpack_int(packed, 4, tensor.shape[-1]) + assert torch.all(unpacked == tensor) + + def test_with_padding(self) -> None: + """14 elements require padding to 16; unpacking strips the pad and recovers the original.""" + tensor = torch.tensor([15, 10, 5, 0, 15, 10, 5, 0, 15, 10, 5, 0, 15, 10], dtype=torch.uint8) + packed = pack_int(tensor, 4) + unpacked = unpack_int(packed, 4, tensor.shape[-1]) + assert torch.all(unpacked == tensor) + + def test_large_random_tensor(self) -> None: + """1000 random 4-bit values survive a pack/unpack round-trip.""" + tensor = torch.randint(0, 16, (1000,), dtype=torch.uint8) + packed = pack_int(tensor, 4) + unpacked = unpack_int(packed, 4, tensor.shape[-1]) + assert torch.all(unpacked == tensor) + + +class TestPackUnpackIntSize8: + """pack_int / unpack_int round-trip for 8-bit integers (full byte, no packing).""" + + def test_full_byte_values(self) -> None: + """16 arbitrary byte values are unchanged by a size-8 pack/unpack round-trip.""" + tensor = torch.tensor([15, 210, 5, 123, 151, 130, 5, 0, 15, 10, 75, 0, 15, 10, 5, 0], dtype=torch.uint8) + packed = pack_int(tensor, 8) + unpacked = unpack_int(packed, 8, tensor.shape[-1]) + assert torch.all(unpacked == tensor) diff --git a/uv.lock b/uv.lock index db74242..081301d 100644 --- a/uv.lock +++ b/uv.lock @@ -412,6 +412,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" }, ] +[[package]] +name = "iniconfig" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, +] + [[package]] name = "jinja2" version = "3.1.6" @@ -1131,6 +1140,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/48/31/05e764397056194206169869b50cf2fee4dbbbc71b344705b9c0d878d4d8/platformdirs-4.9.2-py3-none-any.whl", hash = "sha256:9170634f126f8efdae22fb58ae8a0eaa86f38365bc57897a6c4f781d1f5875bd", size = 21168, upload-time = "2026-02-16T03:56:08.891Z" }, ] +[[package]] +name = "pluggy" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, +] + [[package]] name = "protobuf" version = "6.33.2" @@ -1164,6 +1182,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/88/c5/e98d9c51f3d5300d5e40ad9037dd6b3b60736fd02ab68dcc98c96be7592d/pybind11-3.0.2-py3-none-any.whl", hash = "sha256:f8a6500548919cc33bcd220d5f984688326f574fa97f1107f2f4fdb4c6fb019f", size = 310158, upload-time = "2026-02-17T04:46:49.91Z" }, ] +[[package]] +name = "pygments" +version = "2.19.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" }, +] + [[package]] name = "pyparsing" version = "3.2.5" @@ -1173,6 +1200,22 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/10/5e/1aa9a93198c6b64513c9d7752de7422c06402de6600a8767da1524f9570b/pyparsing-3.2.5-py3-none-any.whl", hash = "sha256:e38a4f02064cf41fe6593d328d0512495ad1f3d8a91c4f73fc401b3079a59a5e", size = 113890, upload-time = "2025-09-21T04:11:04.117Z" }, ] +[[package]] +name = "pytest" +version = "9.0.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d1/db/7ef3487e0fb0049ddb5ce41d3a49c235bf9ad299b6a25d5780a89f19230f/pytest-9.0.2.tar.gz", hash = "sha256:75186651a92bd89611d1d9fc20f0b4345fd827c41ccd5c299a868a05d70edf11", size = 1568901, upload-time = "2025-12-06T21:30:51.014Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3b/ab/b3226f0bd7cdcf710fbede2b3548584366da3b19b5021e74f5bde2a8fa3f/pytest-9.0.2-py3-none-any.whl", hash = "sha256:711ffd45bf766d5264d487b917733b453d917afd2b0ad65223959f59089f875b", size = 374801, upload-time = "2025-12-06T21:30:49.154Z" }, +] + [[package]] name = "python-dateutil" version = "2.9.0.post0" @@ -1259,6 +1302,7 @@ dependencies = [ [package.optional-dependencies] dev = [ { name = "mypy" }, + { name = "pytest" }, { name = "ruff" }, { name = "types-pyyaml" }, ] @@ -1273,6 +1317,7 @@ requires-dist = [ { name = "openfermion", specifier = ">=1.7.1" }, { name = "platformdirs", specifier = ">=4.5.1" }, { name = "pybind11", specifier = ">=3.0.1" }, + { name = "pytest", marker = "extra == 'dev'", specifier = ">=9.0.2" }, { name = "ruff", marker = "extra == 'dev'", specifier = ">=0.14.10" }, { name = "scipy", specifier = ">=1.16.3" }, { name = "tensorboard", specifier = ">=2.20.0" },