Skip to content
Open
Show file tree
Hide file tree
Changes from 9 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
3 changes: 0 additions & 3 deletions source/pip/qsharp/_adaptive_bytecode.py
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,3 @@
REG_TYPE_F32 = 3
REG_TYPE_F64 = 4
REG_TYPE_PTR = 5

# ── Sentinel values ──────────────────────────────────────────────────────────
VOID_RETURN = 0xFFFFFFFF # Function does not have a return value.
78 changes: 50 additions & 28 deletions source/pip/qsharp/_adaptive_pass.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,18 @@

from __future__ import annotations
from dataclasses import dataclass, astuple
from enum import Enum
import pyqir
import struct
from typing import Any, Dict, List, Optional, Tuple, TypeAlias, cast
from ._adaptive_bytecode import *


class Bytecode(Enum):
Bit32 = 32
Bit64 = 64


# ---------------------------------------------------------------------------
# Gate name → OpID mapping (must match shader_types.rs OpID enum)
# ---------------------------------------------------------------------------
Expand Down Expand Up @@ -192,17 +199,20 @@ class SwitchCase:

@dataclass
class IntOperand:
val: int = 0
val: int
bits: int

def __post_init__(self):
# Mask to u32 range so negative Python ints become their
# two's-complement u32 representation (e.g. -7 → 0xFFFFFFF9).
self.val = self.val & 0xFFFFFFFF
# Mask to the appropriate word-width so negative Python ints become
# their two's-complement representation
# (e.g. -7 → 0xFFFFFFF9 for 32-bit, 0xFFFFFFFFFFFFFFF9 for 64-bit).
mask = (1 << self.bits) - 1
self.val = self.val & mask


class FloatOperand:
def __init__(self, val: float = 0.0) -> None:
self.val: int = encode_float_as_bits(val)
def __init__(self, val: float, bytecode_kind: Bytecode) -> None:
self.val: int = encode_float_as_bits(val, bytecode_kind)


@dataclass
Expand Down Expand Up @@ -255,14 +265,24 @@ def unwrap_operands(
return (dst, src0, src1, aux0, aux1, aux2, aux3)


def encode_float_as_bits(val: float) -> int:
return struct.unpack("<I", struct.pack("<f", val))[0]
def encode_float_as_bits(val: float, bytecode_kind: Bytecode) -> int:
if bytecode_kind == Bytecode.Bit32:
return struct.unpack("<I", struct.pack("<f", val))[0]
else:
return struct.unpack("<Q", struct.pack("<d", val))[0]


def void_return(bytecode_kind: Bytecode):
if bytecode_kind == Bytecode.Bit32:
return 0xFFFF_FFFF
else:
return 0xFFFF_FFFF_FFFF_FFFF


class AdaptiveProfilePass:
"""Walks Adaptive Profile QIR and emits the intermediate format for Rust."""

def __init__(self):
def __init__(self, bytecode_kind: Bytecode):
# Output tables.
self.blocks: List[Block] = []
self.instructions: List[Instruction] = []
Expand All @@ -275,6 +295,8 @@ def __init__(self):
self.register_types: List[RegisterType] = []

# Internal tracking.
self._bytecode_kind = bytecode_kind
self._int_bits = bytecode_kind.value
self._next_reg: int = 0
self._next_block: int = 0
self._next_qop: int = 0
Expand Down Expand Up @@ -425,11 +447,11 @@ def _resolve_operand(self, value: pyqir.Value) -> IntOperand | FloatOperand | Re

if isinstance(value, pyqir.IntConstant):
val = value.value
return IntOperand(val)
return IntOperand(val, self._int_bits)

if isinstance(value, pyqir.FloatConstant):
val = value.value
return FloatOperand(val)
return FloatOperand(val, self._bytecode_kind)

# Forward reference (e.g. phi incoming from a later block).
# Pre-allocate a register; the defining instruction will reuse it
Expand All @@ -442,10 +464,10 @@ def _resolve_operand(self, value: pyqir.Value) -> IntOperand | FloatOperand | Re
# Try extracting as a qubit/result pointer constant.
qid = pyqir.qubit_id(value)
if qid is not None:
return IntOperand(qid)
return IntOperand(qid, self._int_bits)
rid = pyqir.result_id(value)
if rid is not None:
return IntOperand(rid)
return IntOperand(rid, self._int_bits)
# Null pointer
if value.is_null:
reg = self._alloc_reg(value, REG_TYPE_PTR)
Expand Down Expand Up @@ -702,7 +724,11 @@ def _emit_call(self, call: pyqir.Call) -> None:
def _resolve_qubit_operands(
self, args: List[pyqir.Value]
) -> Tuple[IntOperand | Reg, IntOperand | Reg, IntOperand | Reg]:
qs: List[IntOperand | Reg] = [IntOperand(), IntOperand(), IntOperand()]
qs: List[IntOperand | Reg] = [
IntOperand(0, self._int_bits),
IntOperand(0, self._int_bits),
IntOperand(0, self._int_bits),
]
for i, arg in enumerate(args):
qs[i] = self._resolve_qubit_operand(arg)
return (qs[0], qs[1], qs[2])
Expand Down Expand Up @@ -752,7 +778,7 @@ def _emit_quantum_call(self, call: pyqir.Call) -> None:
angle = self._resolve_angle_operand(call.args[0])
else:
qubit_arg_offset = 0
angle = FloatOperand()
angle = FloatOperand(0.0, self._bytecode_kind)
qubit_arg_offset = 1 if gate_name in ROTATION_GATES else 0
q1, q2, q3 = self._resolve_qubit_operands(call.args[qubit_arg_offset:])
qop_idx = self._emit_quantum_op(op_id, q1.val, q2.val, q3.val, angle.val)
Expand Down Expand Up @@ -798,8 +824,8 @@ def _emit_noise_intrinsic_call(self, call: pyqir.Call) -> None:
self._emit(
OP_QUANTUM_GATE,
aux0=qop_idx,
aux1=IntOperand(qubit_count),
aux2=IntOperand(arg_offset),
aux1=IntOperand(qubit_count, self._int_bits),
aux2=IntOperand(arg_offset, self._int_bits),
)
elif self._noise_intrinsics is not None:
raise ValueError(f"Missing noise intrinsic: {callee_name}")
Expand Down Expand Up @@ -863,18 +889,14 @@ def _emit_switch(self, switch_instr: pyqir.Switch) -> None:
compilation). ``operands`` is not affected by this behavior.
"""
# operands layout: [cond, default_block, case_val0, case_block0, ...]
ops = switch_instr.operands
cond_reg = self._resolve_operand(ops[0])
default_block = self._block_to_id[ops[1]]
cond_reg = self._resolve_operand(switch_instr.operands[0])
default_block = self._block_to_id[switch_instr.default]
case_offset = len(self.switch_cases)
num_case_pairs = (len(ops) - 2) // 2
for i in range(num_case_pairs):
case_val = ops[2 + 2 * i]
case_block = ops[2 + 2 * i + 1]
target_block = self._block_to_id[case_block]
for case_val, block in switch_instr.cases:
target_block = self._block_to_id[block]
switch_case = SwitchCase(case_val.value, target_block)
self.switch_cases.append(switch_case)
case_count = num_case_pairs
case_count = len(switch_instr.cases)
self._emit(
OP_SWITCH,
src0=cond_reg,
Expand All @@ -899,7 +921,7 @@ def _emit_ret(self, instr: Any) -> None:
self._emit(OP_RET, dst=ret_reg)
else:
# Void return — use immediate 0 as exit code.
self._emit(OP_RET, dst=IntOperand(0))
self._emit(OP_RET, dst=IntOperand(0, self._int_bits))

# ------------------------------------------------------------------
# Comparison emitters
Expand Down Expand Up @@ -963,7 +985,7 @@ def _emit_ir_function_call(self, call: Any) -> None:
self.call_args.append(reg.val)
# Allocate return register if function has non-void return type
if call.type.is_void:
return_reg = VOID_RETURN # no return
return_reg = void_return(self._bytecode_kind) # no return
else:
return_reg = self._alloc_reg(call, REG_TYPE_I32)
self._emit(
Expand Down
19 changes: 19 additions & 0 deletions source/pip/qsharp/_device/_atom/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -258,6 +258,25 @@ def simulate(
if noise is None:
noise = NoiseConfig()

# Override s, s_adj, and z noise if they are unset
# and rz noise is set.
if noise and not noise.rz.is_noiseless():
if noise.s.is_noiseless():
noise.s.x = noise.rz.x
noise.s.y = noise.rz.y
noise.s.z = noise.rz.z
noise.s.loss = noise.rz.loss
if noise.s_adj.is_noiseless():
noise.s_adj.x = noise.rz.x
noise.s_adj.y = noise.rz.y
noise.s_adj.z = noise.rz.z
noise.s_adj.loss = noise.rz.loss
if noise.z.is_noiseless():
noise.z.x = noise.rz.x
noise.z.y = noise.rz.y
noise.z.z = noise.rz.z
noise.z.loss = noise.rz.loss
Comment on lines +261 to +278
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This logic was previously in cpu_simulators.rs, but it is very specific to the device; now that the run_qir function is being used more widely, it makes sense to move it here. Also, the GPU simulator was missing this logic, putting it here makes it common for all simulators available to the NeutralAtomDevice class


compiled = self.compile(qir)
module = Module.from_ir(Context(), str(compiled))
ValidateNoConditionalBranches().run(module)
Expand Down
41 changes: 41 additions & 0 deletions source/pip/qsharp/_native.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -896,6 +896,11 @@ class NoiseTable:
The phase flip noise to use in simulation.
"""

def is_noiseless(self) -> bool:
"""
Returns `true` if there is no noise set.
"""

class NoiseIntrinsicsTable:
def __contains__(self, name: str) -> bool:
"""
Expand Down Expand Up @@ -1005,6 +1010,42 @@ def run_cpu_full_state(
"""
...

def run_cpu_adaptive(
input: dict,
shots: int,
noise: Optional[NoiseConfig] = None,
seed: Optional[int] = None,
) -> List[str]:
"""
Run an adaptive profile QIR program on a CPU full-state simulator.

The input is an `AdaptiveProgram` converted to a dict using the
.as_dict() method. Uses 64-bit bytecode for full LLVM i64 semantics.

Returns a list of result strings. Each result string is composed
of '0's, '1's, and 'L's, representing if each measurement result
was a Zero, One, or Loss respectively.
"""
...

def run_clifford_adaptive(
input: dict,
shots: int,
noise: Optional[NoiseConfig] = None,
seed: Optional[int] = None,
) -> List[str]:
"""
Run an adaptive profile QIR program on a Clifford stabilizer simulator.

The input is an `AdaptiveProgram` converted to a dict using the
.as_dict() method. Uses 64-bit bytecode for full LLVM i64 semantics.

Returns a list of result strings. Each result string is composed
of '0's, '1's, and 'L's, representing if each measurement result
was a Zero, One, or Loss respectively.
"""
...

def try_create_gpu_adapter() -> str:
"""
Checks if a compatible GPU adapter is available on the system.
Expand Down
Loading
Loading