Skip to content

Commit

Permalink
Feat hybrid noise (#60)
Browse files Browse the repository at this point in the history
* Allows noise model to generate hybrid errors (classical and quantum instructions)

* Adding internal cinterp registers for error models

* removing internally generated measurements from results

* Converts conditionals into COps

---------

Co-authored-by: Ciaran Ryan-Anderson <[email protected]>
  • Loading branch information
ciaranra and ciaranra committed Apr 29, 2024
1 parent 5d10ff9 commit 9398b16
Show file tree
Hide file tree
Showing 5 changed files with 273 additions and 193 deletions.
172 changes: 105 additions & 67 deletions python/pecos/classical_interpreters/phir_classical_interpreter.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
from pecos.reps.pypmir import types as pt

if TYPE_CHECKING:
from collections.abc import Generator, Sequence
from collections.abc import Generator, Iterable, Sequence

from pecos import QuantumCircuit
from pecos.foreign_objects.foreign_object_abc import ForeignObject
Expand All @@ -35,12 +35,18 @@ def version2tuple(v):


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_rev = {v: k for k, v in data_type_map.items()}


class PHIRClassicalInterpreter(ClassicalInterpreter):
"""An interpreter that takes in a PHIR program and runs the classical side of the program."""
Expand All @@ -52,6 +58,8 @@ def __init__(self) -> None:
self.foreign_obj = None
self.cenv = None
self.cid2dtype = None
self.csym2id = None
self.cvar_meta = None

self.phir_validate = True

Expand Down Expand Up @@ -96,6 +104,9 @@ def init(self, program: str | (dict | QuantumCircuit), foreign_obj: ForeignObjec

self.check_ffc(self.program.foreign_func_calls, self.foreign_obj)

self.csym2id = dict(self.program.csym2id)
self.cvar_meta = list(self.program.cvar_meta)

self.initialize_cenv()

return self.program.num_qubits
Expand All @@ -121,12 +132,23 @@ def shot_reinit(self):
def initialize_cenv(self) -> None:
self._reset_env()
if self.program:
for cvar in self.program.cvar_meta:
for cvar in self.cvar_meta:
cvar: pt.data.CVarDefine
dtype = data_type_map[cvar.data_type]
self.cenv.append(dtype(0))
self.cid2dtype.append(dtype)

def add_cvar(self, cvar: str, dtype, size: int):
"""Adds a new classical variable to the interpreter."""
if cvar not in self.csym2id:
cid = len(self.csym2id)
self.csym2id[cvar] = cid
self.cenv.append(dtype(0))
self.cid2dtype.append(dtype)
self.cvar_meta.append(
pt.data.CVarDefine(size=size, data_type=data_type_map_rev[dtype], cvar_id=cid, variable=cvar),
)

def _flatten_blocks(self, seq: Sequence):
"""Flattens the ops of blocks to be processed by the execute() method."""
for op in seq:
Expand Down Expand Up @@ -175,83 +197,86 @@ def execute(self, seq: Sequence) -> Generator[list, Any, None]:
yield op_buffer

def get_cval(self, cvar):
cid = self.program.csym2id[cvar]
cid = self.csym2id[cvar]
return self.cenv[cid]

def get_bit(self, cvar, idx):
val = self.get_cval(cvar) & (1 << idx)
val >>= idx
return val

def eval_expr(self, expr: int | (str | (list | dict))) -> int | None:
if isinstance(expr, int):
return expr
elif isinstance(expr, str):
return self.get_cval(expr)
elif isinstance(expr, list):
return self.get_bit(*expr)
elif isinstance(expr, dict):
# TODO: Expressions need to be converted to nested COps!
sym = expr["cop"]
args = expr["args"]

if sym in {"~"}: # Unary ops
lhs = args[0]
rhs = None
else:
lhs, rhs = args
rhs = self.eval_expr(rhs)

lhs = self.eval_expr(lhs)
dtype = type(lhs)

if sym == "^":
return dtype(lhs ^ rhs)
elif sym == "+":
return dtype(lhs + rhs)
elif sym == "-":
return dtype(lhs - rhs)
elif sym == "|":
return dtype(lhs | rhs)
elif sym == "&":
return dtype(lhs & rhs)
elif sym == ">>":
return dtype(lhs >> rhs)
elif sym == "<<":
return dtype(lhs << rhs)
elif sym == "*":
return dtype(lhs * rhs)
elif sym == "/":
return dtype(lhs // rhs)
elif sym == "==":
return dtype(lhs == rhs)
elif sym == "!=":
return dtype(lhs != rhs)
elif sym == "<=":
return dtype(lhs <= rhs)
elif sym == ">=":
return dtype(lhs >= rhs)
elif sym == "<":
return dtype(lhs < rhs)
elif sym == ">":
return dtype(lhs > rhs)
elif sym == "%":
return dtype(lhs % rhs)
elif sym == "~":
return dtype(~lhs)
else:
msg = f"Unknown expression type: {sym}"
raise ValueError(msg)
return None
def eval_expr(self, expr: int | str | list | pt.opt.COp) -> int | None:
"""Evaluates integer expressions."""
match expr:
case int():
return expr

case str():
return self.get_cval(expr)
case list():
return self.get_bit(*expr)
case pt.opt.COp():
sym = expr.name
args = expr.args

if sym in {"~"}: # Unary ops
lhs = args[0]
rhs = None
else:
lhs, rhs = args
rhs = self.eval_expr(rhs)

lhs = self.eval_expr(lhs)
dtype = type(lhs)

if sym == "^":
return dtype(lhs ^ rhs)
elif sym == "+":
return dtype(lhs + rhs)
elif sym == "-":
return dtype(lhs - rhs)
elif sym == "|":
return dtype(lhs | rhs)
elif sym == "&":
return dtype(lhs & rhs)
elif sym == ">>":
return dtype(lhs >> rhs)
elif sym == "<<":
return dtype(lhs << rhs)
elif sym == "*":
return dtype(lhs * rhs)
elif sym == "/":
return dtype(lhs // rhs)
elif sym == "==":
return dtype(lhs == rhs)
elif sym == "!=":
return dtype(lhs != rhs)
elif sym == "<=":
return dtype(lhs <= rhs)
elif sym == ">=":
return dtype(lhs >= rhs)
elif sym == "<":
return dtype(lhs < rhs)
elif sym == ">":
return dtype(lhs > rhs)
elif sym == "%":
return dtype(lhs % rhs)
elif sym == "~":
return dtype(~lhs)
else:
msg = f"Unknown expression type: {sym}"
raise ValueError(msg)
case _:
return None

def assign_int(self, cvar, val: int):
i = None
if isinstance(cvar, (tuple, list)):
cvar, i = cvar

cid = self.program.csym2id[cvar]
cid = self.csym2id[cvar]
dtype = self.cid2dtype[cid]
size = self.program.cvar_meta[cid].size
size = self.cvar_meta[cid].size

cval = self.cenv[cid]
val = dtype(val)
Expand Down Expand Up @@ -312,11 +337,24 @@ def receive_results(self, qsim_results: list[dict]):
def results(self, return_int=True) -> dict:
"""Dumps program final results."""
result = {}
for csym, cid in self.program.csym2id.items():
for csym, cid in self.csym2id.items():
cval = self.cenv[cid]
if not return_int:
size = self.program.cvar_meta[cid].size
size = self.cvar_meta[cid].size
cval = "{:0{width}b}".format(cval, width=size)
result[csym] = cval

return result

def result_bits(self, bits: Iterable[tuple[str, int]], *, filter_private=True) -> dict[tuple[str, int], int]:
"""Git a dictionary of bit values given an iterable of bits (which are encoded as tuple[str, int]
for str[int])."""
send_meas = {}
for b in bits:
for m, i in b:
m: str
i: int
if filter_private and m.startswith("__"):
continue
send_meas[(m, i)] = self.get_bit(m, i)
return send_meas
36 changes: 26 additions & 10 deletions python/pecos/engines/hybrid_engine.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,8 @@ def __init__(
if self.cinterp is None:
self.cinterp = PHIRClassicalInterpreter()

self._internal_cinterp = PHIRClassicalInterpreter()

self.qsim = qsim
if self.qsim is None:
self.qsim = QuantumSimulator()
Expand Down Expand Up @@ -83,6 +85,7 @@ def init(self):
def reset_all(self):
"""Reset to the state of initialization."""
self.cinterp.reset()
self._internal_cinterp.reset()
self.qsim.reset()
self.machine.reset()
self.error_model.reset()
Expand All @@ -99,6 +102,7 @@ def initialize_sim_components(
if foreign_object is not None:
foreign_object.init()
num_qubits = self.cinterp.init(program, foreign_object)
self._internal_cinterp.init(program, foreign_object)
self.machine.init(num_qubits)
self.error_model.init(num_qubits, self.machine)
self.op_processor.init()
Expand All @@ -109,6 +113,9 @@ def shot_reinit_components(self) -> None:
states.
"""
self.cinterp.shot_reinit()
self._internal_cinterp.shot_reinit()
for i in range(self.machine.num_qubits):
self._internal_cinterp.add_cvar(f"__q{i}__", np.uint8, 1)
self.machine.shot_reinit()
self.error_model.shot_reinit()
self.op_processor.shot_reinit()
Expand Down Expand Up @@ -155,27 +162,32 @@ def run(
"""
# TODO: Qubit loss

measurements = MeasData()

if initialize:
self.seed = self.use_seed(seed)
self.initialize_sim_components(program, foreign_object)

for _ in range(shots):
self.shot_reinit_components()

# Execute classical program till quantum sim is needed
# Execute the classical program till quantum sim is needed
for buffered_ops in self.cinterp.execute(self.cinterp.program.ops):
# Process ops, e.g., use `machine` and `error_model` to generate noisy qops
noisy_buffered_qops = self.op_processor.process(buffered_ops)
measurements = self.qsim.run(noisy_buffered_qops)
# Process ops, e.g., use `machine` and `error_model` to generate noisy qops & cops
noisy_buffered_ops = self.op_processor.process(buffered_ops)

# Allows noise to be dependent on measurement outcomes and to alter measurements
measurements = self.op_processor.process_meas(measurements)
# TODO: Think about the safety of evolving the internal registers...
# TODO: Maybe make the error model explicitly declare internal registers...

# TODO: Consider adding the following to generate/evaluate errors after measurement
# measurements, residual_noise = self.op_processor.process_meas(measurements)
# self.simulator.run(residual_noise)
# Process noisy operations
measurements.clear()
for noisy_qops in self._internal_cinterp.execute(noisy_buffered_ops):
temp_meas = self.qsim.run(noisy_qops)
self._internal_cinterp.receive_results(temp_meas)
measurements.extend(temp_meas)

self.cinterp.receive_results(measurements)
transmit_meas = self._internal_cinterp.result_bits(measurements)
self.cinterp.receive_results([transmit_meas])

self.results_accumulator(self.cinterp.results(return_int))

Expand All @@ -198,3 +210,7 @@ def run_multisim(
seed=seed,
pool_size=pool_size,
)


class MeasData(list):
"""Class representing a collection of measurements."""
2 changes: 2 additions & 0 deletions python/pecos/machines/generic_machine.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ class GenericMachine(Machine):

def __init__(self, num_qubits: int | None = None) -> None:
super().__init__(num_qubits=num_qubits)
self.qubit_set = set()
self.leaked_qubits = set()
self.lost_qubits = set()

Expand All @@ -35,6 +36,7 @@ def reset(self) -> None:

def init(self, num_qubits: int | None = None) -> None:
self.num_qubits = num_qubits
self.qubit_set = set(range(num_qubits))

def shot_reinit(self) -> None:
self.reset()
Expand Down
4 changes: 2 additions & 2 deletions python/pecos/reps/pypmir/block_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,8 @@ class IfBlock(Block):
def __init__(
self,
condition: COp,
true_branch: list[COp],
false_branch: list | None = None,
true_branch: list[Op],
false_branch: list[Op] | None = None,
metadata: dict | None = None,
) -> None:
super().__init__(metadata=metadata)
Expand Down
Loading

0 comments on commit 9398b16

Please sign in to comment.