diff --git a/docs/development/DEVELOPMENT.md b/docs/development/DEVELOPMENT.md index 8c7a0df64..377b2a08b 100644 --- a/docs/development/DEVELOPMENT.md +++ b/docs/development/DEVELOPMENT.md @@ -151,3 +151,4 @@ PECOS_HOME = { value = "/custom/path", force = true } For specific development topics, see: - [Parallel Blocks and Optimization](parallel-blocks-and-optimization.md) - Guide to using and extending the Parallel block construct and optimizer +- [PECOS Home Directory Plan](PECOS_HOME_PLAN.md) - External dependency management architecture diff --git a/python/pecos-rslib/pecos_rslib.pyi b/python/pecos-rslib/pecos_rslib.pyi index 750a53a9f..5ff8f7423 100644 --- a/python/pecos-rslib/pecos_rslib.pyi +++ b/python/pecos-rslib/pecos_rslib.pyi @@ -57,9 +57,25 @@ class Scalar: def __rmul__(self, other: Scalar | int | float) -> Scalar: ... def __truediv__(self, other: Scalar | int | float) -> Scalar: ... def __rtruediv__(self, other: Scalar | int | float) -> Scalar: ... + def __floordiv__(self, other: Scalar | int | float) -> Scalar: ... + def __rfloordiv__(self, other: Scalar | int | float) -> Scalar: ... + def __mod__(self, other: Scalar | int | float) -> Scalar: ... + def __rmod__(self, other: Scalar | int | float) -> Scalar: ... def __neg__(self) -> Scalar: ... def __pos__(self) -> Scalar: ... def __abs__(self) -> Scalar: ... + # Bitwise operations (for integer scalar types) + def __and__(self, other: Scalar | int) -> Scalar: ... + def __rand__(self, other: Scalar | int) -> Scalar: ... + def __or__(self, other: Scalar | int) -> Scalar: ... + def __ror__(self, other: Scalar | int) -> Scalar: ... + def __xor__(self, other: Scalar | int) -> Scalar: ... + def __rxor__(self, other: Scalar | int) -> Scalar: ... + def __lshift__(self, other: Scalar | int) -> Scalar: ... + def __rlshift__(self, other: Scalar | int) -> Scalar: ... + def __rshift__(self, other: Scalar | int) -> Scalar: ... + def __rrshift__(self, other: Scalar | int) -> Scalar: ... + def __invert__(self) -> Scalar: ... class ScalarI8(Scalar): """8-bit signed integer scalar.""" diff --git a/python/pecos-rslib/src/dtypes.rs b/python/pecos-rslib/src/dtypes.rs index d836556f2..24d3ba838 100644 --- a/python/pecos-rslib/src/dtypes.rs +++ b/python/pecos-rslib/src/dtypes.rs @@ -3504,40 +3504,131 @@ impl ScalarI64 { } // Bitwise operations - fn __lshift__(&self, other: &Self) -> Self { - Self { - value: self.value << other.value, - } + fn __and__(&self, other: &Bound) -> PyResult { + let other_value = if other.is_instance_of::() { + other.extract::()?.value + } else if let Ok(val) = other.extract::() { + val + } else { + return Err(PyErr::new::(format!( + "unsupported operand type(s) for &: 'i64' and '{}'", + other.get_type().name()? + ))); + }; + Ok(Self { + value: self.value & other_value, + }) } - fn __rshift__(&self, other: &Self) -> Self { - Self { - value: self.value >> other.value, - } + fn __rand__(&self, other: &Bound) -> PyResult { + self.__and__(other) } - fn __and__(&self, other: &Self) -> Self { - Self { - value: self.value & other.value, - } + fn __or__(&self, other: &Bound) -> PyResult { + let other_value = if other.is_instance_of::() { + other.extract::()?.value + } else if let Ok(val) = other.extract::() { + val + } else { + return Err(PyErr::new::(format!( + "unsupported operand type(s) for |: 'i64' and '{}'", + other.get_type().name()? + ))); + }; + Ok(Self { + value: self.value | other_value, + }) } - fn __or__(&self, other: &Self) -> Self { - Self { - value: self.value | other.value, - } + fn __ror__(&self, other: &Bound) -> PyResult { + self.__or__(other) } - fn __xor__(&self, other: &Self) -> Self { - Self { - value: self.value ^ other.value, - } + fn __xor__(&self, other: &Bound) -> PyResult { + let other_value = if other.is_instance_of::() { + other.extract::()?.value + } else if let Ok(val) = other.extract::() { + val + } else { + return Err(PyErr::new::(format!( + "unsupported operand type(s) for ^: 'i64' and '{}'", + other.get_type().name()? + ))); + }; + Ok(Self { + value: self.value ^ other_value, + }) + } + + fn __rxor__(&self, other: &Bound) -> PyResult { + self.__xor__(other) } fn __invert__(&self) -> Self { Self { value: !self.value } } + #[allow(clippy::cast_sign_loss, clippy::cast_possible_truncation)] // Rust shift ops require u32; shifts > 63 are masked by wrapping_shl anyway + fn __lshift__(&self, other: &Bound) -> PyResult { + let shift_amount = if other.is_instance_of::() { + other.extract::()?.value as u32 + } else if let Ok(val) = other.extract::() { + val + } else { + return Err(PyErr::new::(format!( + "unsupported operand type(s) for <<: 'i64' and '{}'", + other.get_type().name()? + ))); + }; + Ok(Self { + value: self.value.wrapping_shl(shift_amount), + }) + } + + #[allow(clippy::cast_sign_loss, clippy::cast_possible_truncation)] // Rust shift ops require u32; shifts > 63 are masked by wrapping_shl anyway + fn __rlshift__(&self, other: &Bound) -> PyResult { + let Ok(base_value) = other.extract::() else { + return Err(PyErr::new::(format!( + "unsupported operand type(s) for <<: '{}' and 'i64'", + other.get_type().name()? + ))); + }; + Ok(Self { + value: base_value.wrapping_shl(self.value as u32), + }) + } + + #[allow(clippy::cast_sign_loss, clippy::cast_possible_truncation)] // Rust shift ops require u32; shifts > 63 are masked by wrapping_shr anyway + fn __rshift__(&self, other: &Bound) -> PyResult { + let shift_amount = if other.is_instance_of::() { + other.extract::()?.value as u32 + } else if let Ok(val) = other.extract::() { + val + } else { + return Err(PyErr::new::(format!( + "unsupported operand type(s) for >>: 'i64' and '{}'", + other.get_type().name()? + ))); + }; + // Arithmetic right shift (sign extension for signed) + Ok(Self { + value: self.value.wrapping_shr(shift_amount), + }) + } + + #[allow(clippy::cast_sign_loss, clippy::cast_possible_truncation)] // Rust shift ops require u32; shifts > 63 are masked by wrapping_shr anyway + fn __rrshift__(&self, other: &Bound) -> PyResult { + let Ok(base_value) = other.extract::() else { + return Err(PyErr::new::(format!( + "unsupported operand type(s) for >>: '{}' and 'i64'", + other.get_type().name()? + ))); + }; + Ok(Self { + value: base_value.wrapping_shr(self.value as u32), + }) + } + // Arithmetic operations with Python int/float fn __add__(&self, other: &Bound) -> PyResult { let other_value = if other.is_instance_of::() { diff --git a/python/quantum-pecos/src/pecos/engines/hybrid_engine.py b/python/quantum-pecos/src/pecos/engines/hybrid_engine.py index f1f195710..f4a7ea770 100644 --- a/python/quantum-pecos/src/pecos/engines/hybrid_engine.py +++ b/python/quantum-pecos/src/pecos/engines/hybrid_engine.py @@ -172,13 +172,30 @@ def shot_reinit_components(self) -> None: self.op_processor.shot_reinit() self.qsim.shot_reinit() - @staticmethod - def use_seed(seed: int | None = None) -> int: - """Use a seed to set random number generators.""" + def use_seed(self, seed: int | None = None) -> int: + """Set a seed for reproducible random number generation. + + This method seeds the global random number generator and stores the seed + on this engine instance. When run() is called without a seed parameter, + it will use this stored seed instead of generating a random one. + + Args: + seed: The seed value to use. If None, a random seed is generated. + + Returns: + The seed value that was used. + + Example: + >>> engine = HybridEngine() + >>> engine.use_seed(42) # Set seed for reproducibility + 42 + >>> results = engine.run(program, shots=100) # Uses seed 42 + """ if seed is None: # Use i32::MAX from Rust as max seed value seed = int(pc.random.randint(0, pc.dtypes.i32.max, 1)[0]) pc.random.seed(seed) + self.seed = seed return seed def results_accumulator(self, shot_results: dict) -> None: @@ -203,7 +220,9 @@ def run( program: The quantum program to execute. foreign_object: Optional foreign object for external function calls. shots: Number of times to run the simulation. - seed: Random seed for reproducibility. + seed: Random seed for reproducibility. If not provided and use_seed() + was previously called, that seed will be used. If neither is set, + a random seed is generated. initialize: Whether to initialize the quantum state before running. return_int: Whether to return measurement results as integers. @@ -211,7 +230,17 @@ def run( measurements = MeasData() if initialize: - self.seed = self.use_seed(seed) + # Use explicit seed if provided, otherwise use previously set seed, + # otherwise generate a random seed + if seed is not None: + self.use_seed(seed) + elif self.seed is None: + # No seed was set via use_seed() and none provided - generate random + self.use_seed(None) + else: + # A seed was previously set via use_seed() - re-seed the RNG with it + # to ensure reproducibility across multiple run() calls + pc.random.seed(self.seed) self.initialize_sim_components(program, foreign_object) for _ in range(shots): diff --git a/python/quantum-pecos/tests/pecos/integration/test_backend_seed_determinism.py b/python/quantum-pecos/tests/pecos/integration/test_backend_seed_determinism.py index 693e19a6c..a5420ab0e 100644 --- a/python/quantum-pecos/tests/pecos/integration/test_backend_seed_determinism.py +++ b/python/quantum-pecos/tests/pecos/integration/test_backend_seed_determinism.py @@ -193,7 +193,7 @@ def test_different_seeds_produce_different_results(backend: str) -> None: # Run with seed=42 engine2 = HybridEngine(qsim=backend, error_model=ERROR_MODEL) engine2.use_seed(42) - results_seed42 = engine1.run(PHIR_BELL_STATE, shots=shots) + results_seed42 = engine2.run(PHIR_BELL_STATE, shots=shots) # Different seeds should produce different sequences assert ( @@ -311,3 +311,28 @@ def test_mps_determinism() -> None: assert ( results1["c"] == results2["c"] ), "MPS: Same seed should produce identical results" + + +@pytest.mark.parametrize("backend", CORE_BACKENDS) +def test_use_seed_equivalent_to_seed_parameter(backend: str) -> None: + """Test that use_seed(N) + run() produces same results as run(seed=N). + + This is a regression test for issue #89. Both methods of setting the seed + should produce identical results for the same seed value. + """ + seed = 42 + shots = 50 + + # Method 1: use_seed() before run() + engine1 = HybridEngine(qsim=backend, error_model=ERROR_MODEL) + engine1.use_seed(seed) + results_method1 = engine1.run(PHIR_BELL_STATE, shots=shots) + + # Method 2: seed parameter in run() + engine2 = HybridEngine(qsim=backend, error_model=ERROR_MODEL) + results_method2 = engine2.run(PHIR_BELL_STATE, shots=shots, seed=seed) + + # Both methods should produce identical results + assert ( + results_method1["c"] == results_method2["c"] + ), f"{backend}: use_seed({seed}) + run() should produce same results as run(seed={seed})" diff --git a/python/quantum-pecos/tests/pecos/integration/test_phir.py b/python/quantum-pecos/tests/pecos/integration/test_phir.py index f1f121d48..28d12204e 100644 --- a/python/quantum-pecos/tests/pecos/integration/test_phir.py +++ b/python/quantum-pecos/tests/pecos/integration/test_phir.py @@ -272,3 +272,48 @@ def test_bell_qparallel_cliff_ifbarrier() -> None: register = "c" if "c" in results_dict else "m" result_values = results_dict[register] assert result_values.count("00") + result_values.count("11") == len(result_values) + + +def test_i64_shift_with_python_int_issue_213() -> None: + """Regression test for issue #213: i64 shift operations with Python int. + + This test reproduces the exact example from the issue where a bitwise + left-shift operation on an i64 variable with a Python int literal failed + with: TypeError: unsupported operand type(s) for <<: 'pecos_rslib.dtypes.i64' and 'int' + + See: https://github.com/PECOS-packages/PECOS/issues/213 + """ + phir = { + "format": "PHIR/JSON", + "version": "0.1.0", + "ops": [ + { + "data": "cvar_define", + "data_type": "i64", + "variable": "a", + "size": 8, + }, + { + "cop": "=", + "returns": ["a"], + "args": [1], + }, + { + "cop": "=", + "returns": ["a"], + "args": [ + { + "cop": "<<", + "args": ["a", 1], + }, + ], + }, + ], + } + + # This should not raise TypeError + results = HybridEngine(qsim="state-vector").run(program=phir, shots=1) + + # Verify the shift operation worked correctly: 1 << 1 = 2 + # Results are returned as binary strings + assert int(results["a"][0], 2) == 2 diff --git a/python/quantum-pecos/tests/pecos/unit/test_phir_classical_interpreter.py b/python/quantum-pecos/tests/pecos/unit/test_phir_classical_interpreter.py index 6d90746ed..21f546e5e 100644 --- a/python/quantum-pecos/tests/pecos/unit/test_phir_classical_interpreter.py +++ b/python/quantum-pecos/tests/pecos/unit/test_phir_classical_interpreter.py @@ -86,3 +86,160 @@ def test_get_bit_out_of_bounds(interpreter: PhirClassicalInterpreter) -> None: match=r"Bit index 1000 out of range for.*\.u64.* \(max 63\)", ): interpreter.get_bit("u64_var", 1000) + + +class TestPhirClassicalInterpreterBitwiseOps: + """Test bitwise operations with PHIR classical interpreter. + + Regression tests for issue #213: i64 shift operations with Python int. + """ + + def test_i64_lshift_with_python_int(self) -> None: + """Test that i64 left shift works with Python int (issue #213).""" + phir_program = { + "format": "PHIR/JSON", + "version": "0.1.0", + "ops": [ + { + "data": "cvar_define", + "data_type": "i64", + "variable": "a", + "size": 64, + }, + {"cop": "=", "args": [1], "returns": ["a"]}, + { + "cop": "=", + "args": [{"cop": "<<", "args": ["a", 1]}], + "returns": ["a"], + }, + ], + } + + interp = PhirClassicalInterpreter() + interp.init(phir_program) + + for _ in interp.execute(interp.program.ops): + pass + + result = interp.results() + assert int(result["a"]) == 2 + + def test_i64_rshift_with_python_int(self) -> None: + """Test that i64 right shift works with Python int.""" + phir_program = { + "format": "PHIR/JSON", + "version": "0.1.0", + "ops": [ + { + "data": "cvar_define", + "data_type": "i64", + "variable": "a", + "size": 64, + }, + {"cop": "=", "args": [16], "returns": ["a"]}, + { + "cop": "=", + "args": [{"cop": ">>", "args": ["a", 2]}], + "returns": ["a"], + }, + ], + } + + interp = PhirClassicalInterpreter() + interp.init(phir_program) + + for _ in interp.execute(interp.program.ops): + pass + + result = interp.results() + assert int(result["a"]) == 4 + + def test_i64_bitwise_and_with_python_int(self) -> None: + """Test that i64 bitwise AND works with Python int.""" + phir_program = { + "format": "PHIR/JSON", + "version": "0.1.0", + "ops": [ + { + "data": "cvar_define", + "data_type": "i64", + "variable": "a", + "size": 64, + }, + {"cop": "=", "args": [15], "returns": ["a"]}, + { + "cop": "=", + "args": [{"cop": "&", "args": ["a", 7]}], + "returns": ["a"], + }, + ], + } + + interp = PhirClassicalInterpreter() + interp.init(phir_program) + + for _ in interp.execute(interp.program.ops): + pass + + result = interp.results() + assert int(result["a"]) == 7 + + def test_i64_bitwise_or_with_python_int(self) -> None: + """Test that i64 bitwise OR works with Python int.""" + phir_program = { + "format": "PHIR/JSON", + "version": "0.1.0", + "ops": [ + { + "data": "cvar_define", + "data_type": "i64", + "variable": "a", + "size": 64, + }, + {"cop": "=", "args": [8], "returns": ["a"]}, + { + "cop": "=", + "args": [{"cop": "|", "args": ["a", 2]}], + "returns": ["a"], + }, + ], + } + + interp = PhirClassicalInterpreter() + interp.init(phir_program) + + for _ in interp.execute(interp.program.ops): + pass + + result = interp.results() + assert int(result["a"]) == 10 + + def test_i64_bitwise_xor_with_python_int(self) -> None: + """Test that i64 bitwise XOR works with Python int.""" + phir_program = { + "format": "PHIR/JSON", + "version": "0.1.0", + "ops": [ + { + "data": "cvar_define", + "data_type": "i64", + "variable": "a", + "size": 64, + }, + {"cop": "=", "args": [15], "returns": ["a"]}, + { + "cop": "=", + "args": [{"cop": "^", "args": ["a", 5]}], + "returns": ["a"], + }, + ], + } + + interp = PhirClassicalInterpreter() + interp.init(phir_program) + + for _ in interp.execute(interp.program.ops): + pass + + result = interp.results() + assert int(result["a"]) == 10