Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
59 commits
Select commit Hold shift + click to select a range
292c227
Move dependency downloads and caching to ~/.pecos/ + developing depen…
ciaranra Dec 13, 2025
63aa39e
making pecos-dev CLI. moving pecos-cli to pecos crate
ciaranra Dec 13, 2025
781671a
update pecos CLI
ciaranra Dec 13, 2025
95a21a6
lint
ciaranra Dec 13, 2025
98c0e05
Fix gitignore to allow llvm source directory to be tracked
ciaranra Dec 13, 2025
03a369d
Fix Windows LLVM 7z extraction to move files to dest
ciaranra Dec 13, 2025
74505d8
Fix Windows 7z extraction - extract directly to dest
ciaranra Dec 13, 2025
bcf3725
separate archives from extracted files + improving pecos-dev CLI
ciaranra Dec 13, 2025
68534b2
pecos-dev improvements
ciaranra Dec 13, 2025
d6e8905
Improve LLVM download retry logic for CI robustness + other CI fixes.
ciaranra Dec 13, 2025
500df48
fix
ciaranra Dec 13, 2025
5db80c4
Reduce language version checks for Julia and Go
ciaranra Dec 13, 2025
0d2c09f
CLI polish
ciaranra Dec 14, 2025
f31c9a7
fix CI
ciaranra Dec 14, 2025
195cdf8
fix
ciaranra Dec 14, 2025
1da1e94
handle cuda better
ciaranra Dec 14, 2025
5a83cd2
fix
ciaranra Dec 15, 2025
3537c04
fix
ciaranra Dec 15, 2025
440c225
fix
ciaranra Dec 15, 2025
212b647
fix
ciaranra Dec 15, 2025
f1f783a
fix
ciaranra Dec 16, 2025
55ef4ab
fix
ciaranra Dec 16, 2025
74f49b2
fix
ciaranra Dec 16, 2025
361e475
fix
ciaranra Dec 16, 2025
a90850d
fix
ciaranra Dec 16, 2025
5082313
fix
ciaranra Dec 16, 2025
3bd32bb
fix
ciaranra Dec 16, 2025
89c4ffb
fix
ciaranra Dec 16, 2025
7d51cf1
fix
ciaranra Dec 16, 2025
bcc4c5e
fix
ciaranra Dec 16, 2025
c690359
polish .gitignore + Makefile
ciaranra Dec 16, 2025
6bf6492
fix
ciaranra Dec 17, 2025
7f72597
fix
ciaranra Dec 17, 2025
78b43eb
fix
ciaranra Dec 17, 2025
10fb1ed
Simplify
ciaranra Dec 17, 2025
19cbb84
fix
ciaranra Dec 17, 2025
5cbb431
fix
ciaranra Dec 17, 2025
55628da
fix
ciaranra Dec 17, 2025
7d978dc
fix
ciaranra Dec 17, 2025
b53c87c
fix
ciaranra Dec 17, 2025
928721f
fix
ciaranra Dec 17, 2025
093f865
fix
ciaranra Dec 18, 2025
ab0fa7f
fix
ciaranra Dec 18, 2025
0d3f64e
fix
ciaranra Dec 18, 2025
5610072
Attempt at fixing #89 and #213
ciaranra Dec 18, 2025
7210444
fix
ciaranra Dec 18, 2025
3cd0b1b
fix
ciaranra Dec 18, 2025
672cbe8
fix
ciaranra Dec 18, 2025
bcf0be1
fix
ciaranra Dec 18, 2025
dd32a40
fix
ciaranra Dec 18, 2025
a8faf94
fix
ciaranra Dec 18, 2025
d8aa86a
fix
ciaranra Dec 18, 2025
893c2f7
Move cleaning to Python script
ciaranra Dec 19, 2025
85e5054
Switch to Just
ciaranra Dec 19, 2025
732c5f5
Merge remote-tracking branch 'origin/feat/pecos-deps' into bug/issues…
ciaranra Dec 19, 2025
3d3161a
fix
ciaranra Dec 19, 2025
bee485d
Merge branch 'feat/pecos-deps' into bug/issues-213-and-89
ciaranra Dec 19, 2025
6df4f38
Merge branch 'dev' into bug/issues-213-and-89
ciaranra Dec 20, 2025
921df61
Merge remote-tracking branch 'origin/dev' into bug/issues-213-and-89
ciaranra Dec 24, 2025
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
1 change: 1 addition & 0 deletions docs/development/DEVELOPMENT.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
16 changes: 16 additions & 0 deletions python/pecos-rslib/pecos_rslib.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -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."""
Expand Down
131 changes: 111 additions & 20 deletions python/pecos-rslib/src/dtypes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<PyAny>) -> PyResult<Self> {
let other_value = if other.is_instance_of::<ScalarI64>() {
other.extract::<ScalarI64>()?.value
} else if let Ok(val) = other.extract::<i64>() {
val
} else {
return Err(PyErr::new::<pyo3::exceptions::PyTypeError, _>(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<PyAny>) -> PyResult<Self> {
self.__and__(other)
}

fn __and__(&self, other: &Self) -> Self {
Self {
value: self.value & other.value,
}
fn __or__(&self, other: &Bound<PyAny>) -> PyResult<Self> {
let other_value = if other.is_instance_of::<ScalarI64>() {
other.extract::<ScalarI64>()?.value
} else if let Ok(val) = other.extract::<i64>() {
val
} else {
return Err(PyErr::new::<pyo3::exceptions::PyTypeError, _>(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<PyAny>) -> PyResult<Self> {
self.__or__(other)
}

fn __xor__(&self, other: &Self) -> Self {
Self {
value: self.value ^ other.value,
}
fn __xor__(&self, other: &Bound<PyAny>) -> PyResult<Self> {
let other_value = if other.is_instance_of::<ScalarI64>() {
other.extract::<ScalarI64>()?.value
} else if let Ok(val) = other.extract::<i64>() {
val
} else {
return Err(PyErr::new::<pyo3::exceptions::PyTypeError, _>(format!(
"unsupported operand type(s) for ^: 'i64' and '{}'",
other.get_type().name()?
)));
};
Ok(Self {
value: self.value ^ other_value,
})
}

fn __rxor__(&self, other: &Bound<PyAny>) -> PyResult<Self> {
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<PyAny>) -> PyResult<Self> {
let shift_amount = if other.is_instance_of::<ScalarI64>() {
other.extract::<ScalarI64>()?.value as u32
} else if let Ok(val) = other.extract::<u32>() {
val
} else {
return Err(PyErr::new::<pyo3::exceptions::PyTypeError, _>(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<PyAny>) -> PyResult<Self> {
let Ok(base_value) = other.extract::<i64>() else {
return Err(PyErr::new::<pyo3::exceptions::PyTypeError, _>(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<PyAny>) -> PyResult<Self> {
let shift_amount = if other.is_instance_of::<ScalarI64>() {
other.extract::<ScalarI64>()?.value as u32
} else if let Ok(val) = other.extract::<u32>() {
val
} else {
return Err(PyErr::new::<pyo3::exceptions::PyTypeError, _>(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<PyAny>) -> PyResult<Self> {
let Ok(base_value) = other.extract::<i64>() else {
return Err(PyErr::new::<pyo3::exceptions::PyTypeError, _>(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<PyAny>) -> PyResult<Self> {
let other_value = if other.is_instance_of::<ScalarI64>() {
Expand Down
39 changes: 34 additions & 5 deletions python/quantum-pecos/src/pecos/engines/hybrid_engine.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -203,15 +220,27 @@ 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.

"""
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):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand Down Expand Up @@ -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})"
45 changes: 45 additions & 0 deletions python/quantum-pecos/tests/pecos/integration/test_phir.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Loading
Loading