diff --git a/Makefile b/Makefile index 0a7132fd..ca13d66f 100644 --- a/Makefile +++ b/Makefile @@ -28,7 +28,7 @@ updatereqs: upgrade-pip ## Autogenerate requirements.txt $(VENV_BIN)/pip-compile --extra=tests --no-annotate --no-emit-index-url --output-file=requirements.txt --strip-extras pyproject.toml metadeps: upgrade-pip ## Install extra dependencies used to develop/build this package - $(VENV_BIN)/pip install -U build pip-tools pre-commit wheel pytest + $(VENV_BIN)/pip install -U build pip-tools pre-commit wheel pytest hypothesis # Installation # ------------ diff --git a/pyproject.toml b/pyproject.toml index 04fe7535..30cf393e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -15,7 +15,7 @@ build-backend = "setuptools.build_meta" [project] name = "quantum-pecos" -version = "0.6.0.dev1" +version = "0.6.0.dev2" authors = [ {name = "The PECOS Developers"}, ] @@ -28,7 +28,7 @@ requires-python = ">=3.10" license = { file = "LICENSE"} keywords = ["quantum", "QEC", "simulation", "PECOS"] dependencies = [ - "phir~=0.3.0", + "phir>=0.3.3,<0.4", "numpy>=1.15.0,<2.0", "scipy>=1.1.0,<2.0", "networkx>=2.1.0,<3.0", @@ -73,7 +73,8 @@ visualization = [ "plotly~=5.9.0", ] tests = [ - "pytest>=5.0.0" + "pytest>=5.0.0", + "hypothesis" ] all = [ "quantum-pecos[simulators]", diff --git a/python/pecos/__init__.py b/python/pecos/__init__.py index 4a9a68fd..618b09c0 100644 --- a/python/pecos/__init__.py +++ b/python/pecos/__init__.py @@ -29,7 +29,7 @@ from pecos import circuit_converters, circuits, decoders, engines, error_models, misc, qeccs, simulators, tools from pecos.circuits.quantum_circuit import QuantumCircuit from pecos.engines import circuit_runners -from pecos.engines.cvm.binarray import BinArray +from pecos.engines.cvm.binarray2 import BinArray2 as BinArray from pecos.engines.hybrid_engine_old import HybridEngine __all__ = [ diff --git a/python/pecos/classical_interpreters/phir_classical_interpreter.py b/python/pecos/classical_interpreters/phir_classical_interpreter.py index 502c265e..14a623fd 100644 --- a/python/pecos/classical_interpreters/phir_classical_interpreter.py +++ b/python/pecos/classical_interpreters/phir_classical_interpreter.py @@ -15,11 +15,10 @@ import warnings from typing import TYPE_CHECKING, Any -import numpy as np from phir.model import PHIRModel from pecos.classical_interpreters.classical_interpreter_abc import ClassicalInterpreter -from pecos.reps.pypmir import PyPMIR +from pecos.reps.pypmir import PyPMIR, signed_data_types, unsigned_data_types from pecos.reps.pypmir import types as pt if TYPE_CHECKING: @@ -34,16 +33,7 @@ def version2tuple(v): return tuple(map(int, (v.split(".")))) -data_type_map = { - "i8": np.int8, - "i16": np.int16, - "i32": np.int32, - "i64": np.int64, - "u8": np.uint8, - "u16": np.uint16, - "u32": np.uint32, - "u64": np.uint64, -} +data_type_map = signed_data_types | unsigned_data_types data_type_map_rev = {v: k for k, v in data_type_map.items()} @@ -276,7 +266,6 @@ def assign_int(self, cvar, val: int): cid = self.csym2id[cvar] dtype = self.cid2dtype[cid] - size = self.cvar_meta[cid].size cval = self.cenv[cid] val = dtype(val) @@ -286,8 +275,11 @@ def assign_int(self, cvar, val: int): cval &= ~(1 << i) cval |= (val & 1) << i - # mask off bits give the size of the register - cval &= (1 << size) - 1 + if type(cval) not in signed_data_types.values(): + # mask off bits given the size of the register + # (only valid for unsigned data types) + size = self.cvar_meta[cid].size + cval &= (1 << size) - 1 self.cenv[cid] = cval def handle_cops(self, op): diff --git a/python/pecos/engines/cvm/binarray2.py b/python/pecos/engines/cvm/binarray2.py index 4a66120b..31364524 100644 --- a/python/pecos/engines/cvm/binarray2.py +++ b/python/pecos/engines/cvm/binarray2.py @@ -13,9 +13,13 @@ import numpy as np +from pecos.reps.pypmir import unsigned_data_types + class BinArray2: - def __init__(self, size, value=0, dtype=np.int32) -> None: + """As opposed to the original unsigned 32-bit BinArray, this class defaults to signed 64-bit type.""" + + def __init__(self, size, value=0, dtype=np.int64) -> None: self.size = size self.value = None self.dtype = dtype @@ -33,6 +37,8 @@ def __init__(self, size, value=0, dtype=np.int32) -> None: def set(self, value): if isinstance(value, self.dtype): self.value = value + elif isinstance(value, BinArray2): + self.value = value.value else: if isinstance(value, str): value = int(value, 2) @@ -41,7 +47,8 @@ def set(self, value): def new_val(self, value): b = BinArray2(self.size, value, self.dtype) - b.clamp(self.size) + if self.dtype in unsigned_data_types.values(): + b.clamp(self.size) return b def num_bits(self): diff --git a/python/pecos/engines/cvm/classical.py b/python/pecos/engines/cvm/classical.py index f7443e8d..f810eac8 100644 --- a/python/pecos/engines/cvm/classical.py +++ b/python/pecos/engines/cvm/classical.py @@ -11,7 +11,7 @@ from __future__ import annotations -from pecos.engines.cvm.binarray import BinArray +from pecos.engines.cvm.binarray2 import BinArray2 as BinArray def set_output(state, circuit, output_spec, output): diff --git a/python/pecos/engines/cvm/wasm.py b/python/pecos/engines/cvm/wasm.py index 4ab5bda0..5383bdf5 100644 --- a/python/pecos/engines/cvm/wasm.py +++ b/python/pecos/engines/cvm/wasm.py @@ -12,7 +12,7 @@ import pickle from pathlib import Path -from pecos.engines.cvm.binarray import BinArray +from pecos.engines.cvm.binarray2 import BinArray2 as BinArray from pecos.engines.cvm.sim_func import sim_exec from pecos.engines.cvm.wasm_vms.pywasm import read_pywasm from pecos.engines.cvm.wasm_vms.pywasm3 import read_pywasm3 diff --git a/python/pecos/engines/hybrid_engine_old.py b/python/pecos/engines/hybrid_engine_old.py index f322c9a3..4754c609 100644 --- a/python/pecos/engines/hybrid_engine_old.py +++ b/python/pecos/engines/hybrid_engine_old.py @@ -15,7 +15,8 @@ import numpy as np -from pecos.engines.cvm.classical import BinArray, eval_condition, eval_cop, set_output +from pecos.engines.cvm.binarray2 import BinArray2 as BinArray +from pecos.engines.cvm.classical import eval_condition, eval_cop, set_output from pecos.engines.cvm.wasm import eval_cfunc, get_ccop from pecos.error_models.fake_error_model import FakeErrorModel from pecos.errors import NotSupportedGateError diff --git a/python/pecos/reps/pypmir/__init__.py b/python/pecos/reps/pypmir/__init__.py index 76527bee..0800c3b8 100644 --- a/python/pecos/reps/pypmir/__init__.py +++ b/python/pecos/reps/pypmir/__init__.py @@ -9,4 +9,4 @@ # "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 pecos.reps.pypmir.pypmir import PyPMIR +from pecos.reps.pypmir.pypmir import PyPMIR, signed_data_types, unsigned_data_types diff --git a/python/pecos/reps/pypmir/pypmir.py b/python/pecos/reps/pypmir/pypmir.py index 1f83d3ca..80c87be9 100644 --- a/python/pecos/reps/pypmir/pypmir.py +++ b/python/pecos/reps/pypmir/pypmir.py @@ -15,6 +15,8 @@ from math import pi from typing import TYPE_CHECKING, Callable, TypeVar +import numpy as np + from pecos.reps.pypmir import block_types as blk from pecos.reps.pypmir import data_types as d from pecos.reps.pypmir import op_types as op @@ -25,6 +27,20 @@ TypeOp = TypeVar("TypeOp", bound=op.Op) +signed_data_types = { + "i8": np.int8, + "i16": np.int16, + "i32": np.int32, + "i64": np.int64, +} + +unsigned_data_types = { + "u8": np.uint8, + "u16": np.uint16, + "u32": np.uint32, + "u64": np.uint64, +} + class PyPMIR: """Pythonic PECOS Middle-level IR. Used to convert PHIR into an object and optimize the data structure for @@ -229,10 +245,14 @@ def from_phir(cls, phir: dict, name_resolver=None) -> PyPMIR: name = o["data"] if name == "cvar_define": + data_type = o["data_type"] + size = int(data_type[1:]) + if "size" in o: + size = o["size"] data = d.CVarDefine( - data_type=o["data_type"], + data_type=data_type, variable=o["variable"], - size=o["size"], + size=size, cvar_id=len(p.cvar_meta), metadata=o.get("metadata"), ) diff --git a/requirements.txt b/requirements.txt index 6b4b606f..d0d27e16 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,9 +5,11 @@ # pip-compile --extra=tests --no-annotate --no-emit-index-url --output-file=requirements.txt --strip-extras pyproject.toml # annotated-types==0.6.0 +attrs==23.2.0 contourpy==1.2.0 cycler==0.12.1 fonttools==4.50.0 +hypothesis==6.100.1 iniconfig==2.0.0 kiwisolver==1.4.5 markdown-it-py==3.0.0 @@ -16,7 +18,7 @@ mdurl==0.1.2 networkx==2.8.8 numpy==1.26.4 packaging==24.0 -phir==0.3.2 +phir==0.3.3 pillow==10.2.0 pluggy==1.4.0 pydantic==2.6.4 @@ -28,4 +30,5 @@ python-dateutil==2.9.0.post0 rich==13.7.1 scipy==1.12.0 six==1.16.0 +sortedcontainers==2.4.0 typing-extensions==4.10.0 diff --git a/tests/integration/test_phir_64_bit.py b/tests/integration/test_phir_64_bit.py new file mode 100644 index 00000000..66ea4929 --- /dev/null +++ b/tests/integration/test_phir_64_bit.py @@ -0,0 +1,35 @@ +from pecos.engines.hybrid_engine import HybridEngine + + +def bin2int(result: list[str]) -> int: + return int(result[0], base=2) + + +def test_setting_cvar(): + phir = { + "format": "PHIR/JSON", + "version": "0.1.0", + "ops": [ + {"data": "cvar_define", "data_type": "i32", "variable": "var_i32"}, + {"data": "cvar_define", "data_type": "u32", "variable": "var_u32", "size": 32}, + {"data": "cvar_define", "data_type": "i64", "variable": "var_i64"}, + {"data": "cvar_define", "data_type": "u64", "variable": "var_u64", "size": 64}, + {"data": "cvar_define", "data_type": "i32", "variable": "var_i32neg"}, + {"data": "cvar_define", "data_type": "i64", "variable": "var_i64neg"}, + {"cop": "=", "returns": ["var_i32"], "args": [2**31 - 1]}, + {"cop": "=", "returns": ["var_u32"], "args": [2**32 - 1]}, + {"cop": "=", "returns": ["var_i64"], "args": [2**63 - 1]}, + {"cop": "=", "returns": ["var_u64"], "args": [2**64 - 1]}, + {"cop": "=", "returns": ["var_i32neg"], "args": [-(2**31)]}, + {"cop": "=", "returns": ["var_i64neg"], "args": [-(2**63)]}, + ], + } + + results = HybridEngine(qsim="stabilizer").run(program=phir, shots=5) + + assert bin2int(results["var_i32"]) == 2**31 - 1 + assert bin2int(results["var_u32"]) == 2**32 - 1 + assert bin2int(results["var_i64"]) == 2**63 - 1 + assert bin2int(results["var_u64"]) == 2**64 - 1 + assert bin2int(results["var_i32neg"]) == -(2**31) + assert bin2int(results["var_i64neg"]) == -(2**63) diff --git a/tests/unit/test_binarray.py b/tests/unit/test_binarray.py new file mode 100644 index 00000000..d6f013d2 --- /dev/null +++ b/tests/unit/test_binarray.py @@ -0,0 +1,104 @@ +from typing import Final + +import numpy as np +from hypothesis import assume, given +from hypothesis import strategies as st +from pecos.engines.cvm.binarray2 import BinArray2 as BinArray + +DEFAULT_SIZE: Final = 63 +MIN: Final = -(2**DEFAULT_SIZE) +MAX: Final = 2**DEFAULT_SIZE - 1 +int_range = st.integers(min_value=MIN, max_value=MAX) + + +@given(st.text(alphabet=["0", "1"], min_size=1)) +def test_init(x): + ba = BinArray(x) + assert ba == f"0b{x}" + + +def test_set_bit(): + ba = BinArray("0000") + ba[2] = 1 + assert ba == 0b0100 + + +def test_get_bit(): + ba = BinArray("1010") + assert ba[2] == 0 + assert ba[3] == 1 + + +def test_to_int(): + ba = BinArray("1010") + assert int(ba) == 10 + + +@given(int_range, int_range) +def test_addition(x, y): + assume(MIN <= x + y <= MAX) + ba1 = BinArray(DEFAULT_SIZE, x) + ba2 = BinArray(DEFAULT_SIZE, y) + result = ba1 + ba2 + assert int(result) == x + y + + +def test_subtraction(): + ba1 = BinArray("1101") # 13 + ba2 = BinArray("1010") # 10 + result = ba1 - ba2 + assert int(result) == 3 + + +@given(int_range, int_range) +def test_multiplication(x, y): + assume(MIN <= x * y <= MAX) + ba1 = BinArray(DEFAULT_SIZE, x) + ba2 = BinArray(DEFAULT_SIZE, y) + result = ba1 * ba2 + assert int(result) == x * y + + +def test_comparison(): + ba1 = BinArray("1010") # 10 + ba2 = BinArray("1010") # 10 + ba3 = BinArray("1101") # 13 + assert ba1 == ba2 + assert ba1 != ba3 + assert ba1 != ba3 + assert ba1 < ba3 + assert ba3 > ba1 + + +def test_bitwise_and(): + ba1 = BinArray("1010") # 10 + ba2 = BinArray("1101") # 13 + result = ba1 & ba2 + assert result == 0b1000 + + +def test_bitwise_or(): + ba1 = BinArray("1010") # 10 + ba2 = BinArray("1101") # 13 + result = ba1 | ba2 + assert result == 0b1111 + + +def test_bitwise_xor(): + ba1 = BinArray("1010") # 10 + ba2 = BinArray("1101") # 13 + result = ba1 ^ ba2 + assert result == 0b0111 + + +def test_unsigned_bitwise_not(): + ba = BinArray("1010", dtype=np.uint64) # 10 + result = ~ba + assert result == 0b0101 + + +@given(int_range) +def test_signed_bitwise_not(x): + ba = BinArray(DEFAULT_SIZE, x) + result = ~ba + assert int(result) == -x - 1 # (two's complement)