diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 799e4d9a38b..a93ac748ee6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -93,6 +93,19 @@ jobs: - name: npm check run: npm run check + python-check: + name: Python static checks + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + with: + submodules: "true" + - uses: actions/setup-python@v6 + with: + python-version: ${{ env.PYTHON_VERSION }} + - name: Run python static checks + run: python ./build.py --check-py --no-check-prereqs + build: name: Build and test runs-on: ubuntu-latest @@ -203,6 +216,7 @@ jobs: format, clippy, web-check, + python-check, build, unit-tests, format-qsc, diff --git a/build.py b/build.py index 8edf636f237..b05cf59fb50 100755 --- a/build.py +++ b/build.py @@ -63,6 +63,13 @@ help="Run the prerequisites check (default is --check-prereqs)", ) +parser.add_argument( + "--check-py", + action=argparse.BooleanOptionalAction, + default=False, + help="Run static checks (linting, formatting and typing) for Python and nothing else", +) + parser.add_argument( "--integration-tests", action=argparse.BooleanOptionalAction, @@ -269,6 +276,48 @@ def use_python_env(folder): return (python_bin, pip_env) +# Static checks for Python code in source/qdk_package. +# Currently only includes type checking with pyright. +# Runs if: +# 1. Enabled explicitly (will run these checks only): build.py --check-py. +# 2. Or if this build builds qdk package and checks are not disabled. +def run_python_checks(): + python_bin, env = use_python_env(qdk_python_src) + + step_start("Install requirements for Python static checks") + run( + [ + python_bin, + "-m", + "pip", + "install", + "-r", + "check_requirements.txt", + "-q", + ], + cwd=qdk_python_src, + env=env, + ) + step_end() + + step_start("Running pyright checks") + run( + [ + python_bin, + "-m", + "pyright", + ], + cwd=root_dir, + env=env, + ) + step_end() + + +if args.check_py: + run_python_checks() + exit(0) + + if npm_install_needed: command = [npm_cmd, "install"] if not args.optional_dependencies: @@ -330,6 +379,10 @@ def use_python_env(folder): ) step_end() + if build_qdk: + run_python_checks() + + if build_cli: if run_tests: step_start("Running Rust unit tests") diff --git a/pyrightconfig.json b/pyrightconfig.json index 18db637af3b..2a2792db5a6 100644 --- a/pyrightconfig.json +++ b/pyrightconfig.json @@ -1,5 +1,14 @@ { "pythonVersion": "3.10", - "extraPaths": ["source/pip", "source/widgets/src", "source/qdk_package/src"], + "include": ["source/qdk_package/qdk"], + "exclude": [ + "source/qdk_package/qdk/applications", + "source/qdk_package/qdk/azure", + "source/qdk_package/qdk/cirq", + "source/qdk_package/qdk/simulation", + "source/qdk_package/qdk/qiskit", + "source/qdk_package/qdk/qre" + ], + "reportMissingModuleSource": "none", // Allow .pyi without .py "typeCheckingMode": "basic" } diff --git a/source/qdk_package/check_requirements.txt b/source/qdk_package/check_requirements.txt new file mode 100644 index 00000000000..144e265be2c --- /dev/null +++ b/source/qdk_package/check_requirements.txt @@ -0,0 +1,6 @@ +# Libraries that need to be installed to run Python static checks. +matplotlib +pandas>=2.1 +pandas-stubs +pyqir>=0.12.5,<0.13 +pyright==1.1.410 diff --git a/source/qdk_package/qdk/_adaptive_pass.py b/source/qdk_package/qdk/_adaptive_pass.py index 0ae601364c0..733205b3d71 100644 --- a/source/qdk_package/qdk/_adaptive_pass.py +++ b/source/qdk_package/qdk/_adaptive_pass.py @@ -478,12 +478,12 @@ def _resolve_operand(self, value: pyqir.Value) -> IntOperand | FloatOperand | Re return self._value_to_reg[value] if isinstance(value, pyqir.IntConstant): - val = value.value - return IntOperand(val, self._int_bits) + int_val = value.value + return IntOperand(int_val, self._int_bits) if isinstance(value, pyqir.FloatConstant): - val = value.value - return FloatOperand(val, self._bytecode_kind) + float_val = value.value + return FloatOperand(float_val, self._bytecode_kind) # Global variable reference (e.g., @array0) if hasattr(value, "name") and value.name in self._global_to_address: @@ -895,7 +895,7 @@ def _emit_call(self, call: pyqir.Call) -> None: self._emit_quantum_call(call) case _ if callee in self._func_to_id: self._emit_ir_function_call(call) - case _ if "qdk_noise" in call.callee.attributes.func: + case _ if "qdk_noise" in cast(pyqir.Function, call.callee).attributes.func: # Check if this is a noise intrinsic (custom gate with qdk_noise attribute) self._emit_noise_intrinsic_call(call) case _: @@ -964,11 +964,13 @@ def _emit_quantum_call(self, call: pyqir.Call) -> None: # the runtime invokes ``Simulator::mov`` (which applies the # configured ``noise.mov`` faults to that qubit). q1, q2, q3 = self._resolve_qubit_operands([call.args[0]]) - angle = FloatOperand(0.0, self._bytecode_kind) - qop_idx = self._emit_quantum_op(op_id, q1.val, q2.val, q3.val, angle.val) + move_angle = FloatOperand(0.0, self._bytecode_kind) + qop_idx = self._emit_quantum_op( + op_id, q1.val, q2.val, q3.val, move_angle.val + ) self._emit( OP_QUANTUM_GATE, - src0=angle, + src0=move_angle, aux0=qop_idx, aux1=q1, aux2=q2, @@ -977,11 +979,10 @@ def _emit_quantum_call(self, call: pyqir.Call) -> None: return if gate_name in ROTATION_GATES: qubit_arg_offset = 1 - angle = self._resolve_angle_operand(call.args[0]) + angle: FloatOperand | Reg = self._resolve_angle_operand(call.args[0]) else: qubit_arg_offset = 0 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) self._emit( diff --git a/source/qdk_package/qdk/_context.py b/source/qdk_package/qdk/_context.py index af232db1ace..bb7d514697b 100644 --- a/source/qdk_package/qdk/_context.py +++ b/source/qdk_package/qdk/_context.py @@ -10,6 +10,7 @@ """ import json +import builtins import sys import types import weakref @@ -66,7 +67,7 @@ # Check if we are running in a Jupyter notebook to use the IPython display function _in_jupyter = False try: - from IPython.display import display + from IPython.display import display # type: ignore[import-not-found] if get_ipython().__class__.__name__ == "ZMQInteractiveShell": # type: ignore _in_jupyter = True # Jupyter notebook or qtconsole @@ -81,7 +82,7 @@ def ipython_helper(): try: if __IPYTHON__: # type: ignore - from IPython.display import display + from IPython.display import display # type: ignore[import-not-found] except NameError: pass @@ -106,9 +107,9 @@ def _visual_circuit_count(circuit_json: str) -> int: def make_class_rec(qsharp_type: TypeIR) -> type: class_name = qsharp_type.unwrap_udt().name - fields = {} + fields: dict[str, Any] = {} for field in qsharp_type.unwrap_udt().fields: - ty = None + ty: type[Any] | None = None kind = field[1].kind() if kind == TypeKind.Primitive: @@ -141,6 +142,7 @@ def make_class_rec(qsharp_type: TypeIR) -> type: ty = make_class_rec(field[1]) else: raise QSharpError(f"unknown type {kind}") + assert ty is not None fields[field[0]] = ty return make_dataclass( @@ -384,7 +386,7 @@ def _lower_python_obj( finally: visited.remove(obj_id) - def _python_args_to_interpreter_args(self, args: tuple[Any, ...]): + def _python_args_to_interpreter_args(self, args: tuple[Any, ...]) -> Any: """Turns `args` to the format expected by the Q# interpreter.""" if len(args) == 0: return None @@ -408,7 +410,7 @@ def _get_code_module( self, namespace: List[str] ) -> types.ModuleType | types.SimpleNamespace: """Returns module for given path, creating it if it doesn't exist.""" - module = self.code + module: Any = self.code if self._is_global_context: accumulated_namespace = code.__name__ for name in namespace: @@ -427,9 +429,9 @@ def _get_code_module( if hasattr(module, name): module = module.__getattribute__(name) else: - new_module = types.SimpleNamespace() - module.__setattr__(name, new_module) - module = new_module + new_namespace_module = types.SimpleNamespace() + module.__setattr__(name, new_namespace_module) + module = new_namespace_module return module def _make_callable( @@ -465,7 +467,7 @@ def _make_class(self, qsharp_type: TypeIR, namespace: List[str], class_name: str """Registers a Q# type as a Python dataclass in this context's code module.""" module = self._get_code_module(namespace) QSharpClass = make_class_rec(qsharp_type) - QSharpClass.__qsharp_class = True + setattr(QSharpClass, "__qsharp_class", True) setattr(QSharpClass, "_qdk_context", self) module.__setattr__(class_name, QSharpClass) @@ -527,7 +529,9 @@ def import_circuit( try: _, circuit_json = read_file(resolved_path) except Exception as err: - raise QSharpError(f"Error reading visual circuit file {resolved_path}.") from err + raise QSharpError( + f"Error reading visual circuit file {resolved_path}." + ) from err circuit_count = _visual_circuit_count(circuit_json) operation_base_name = name if name is not None else _path_stem(resolved_path) @@ -684,9 +688,7 @@ def on_save_events(output: Output) -> None: callable = None run_entry_expr = None - if isinstance(entry_expr, Callable) and hasattr( - entry_expr, "__global_callable" - ): + if builtins.callable(entry_expr) and hasattr(entry_expr, "__global_callable"): self._check_same_context_callable(entry_expr) args = self._python_args_to_interpreter_args(args) callable = getattr(entry_expr, "__global_callable") @@ -775,9 +777,7 @@ def compile( start = monotonic() target_profile = self._config.get_target_profile() telemetry_events.on_compile(target_profile) - if isinstance(entry_expr, Callable) and hasattr( - entry_expr, "__global_callable" - ): + if builtins.callable(entry_expr) and hasattr(entry_expr, "__global_callable"): self._check_same_context_callable(entry_expr) args = self._python_args_to_interpreter_args(args) ll_str = self._interpreter.qir( @@ -861,9 +861,7 @@ def circuit( prune_classical_qubits=prune_classical_qubits, ) - if isinstance(entry_expr, Callable) and hasattr( - entry_expr, "__global_callable" - ): + if builtins.callable(entry_expr) and hasattr(entry_expr, "__global_callable"): self._check_same_context_callable(entry_expr) args = self._python_args_to_interpreter_args(args) res = self._interpreter.circuit( @@ -872,9 +870,10 @@ def circuit( args=args, ) elif isinstance(entry_expr, (GlobalCallable, Closure)): - args = self._python_args_to_interpreter_args(args) res = self._interpreter.circuit( - config=config, callable=entry_expr, args=args + config=config, + callable=entry_expr, + args=self._python_args_to_interpreter_args(args), ) else: assert entry_expr is None or isinstance(entry_expr, str) @@ -902,9 +901,7 @@ def logical_counts( """ ipython_helper() - if isinstance(entry_expr, Callable) and hasattr( - entry_expr, "__global_callable" - ): + if builtins.callable(entry_expr) and hasattr(entry_expr, "__global_callable"): self._check_same_context_callable(entry_expr) args = self._python_args_to_interpreter_args(args) res_dict = self._interpreter.logical_counts( diff --git a/source/qdk_package/qdk/_device/_atom/__init__.py b/source/qdk_package/qdk/_device/_atom/__init__.py index 5585c7b19ca..d58716320ea 100644 --- a/source/qdk_package/qdk/_device/_atom/__init__.py +++ b/source/qdk_package/qdk/_device/_atom/__init__.py @@ -198,7 +198,7 @@ def show_trace(self, qir: str | QirInputData): """ try: - from qsharp_widgets import Atoms + from qsharp_widgets import Atoms # type: ignore[import-not-found] except ImportError: raise ImportError( "The qsharp-widgets package is required for showing atom trace visualization. " @@ -208,7 +208,7 @@ def show_trace(self, qir: str | QirInputData): from ._validate import ValidateNoConditionalBranches from ._scheduler import Schedule from pyqir import Module, Context - from IPython.display import display + from IPython.display import display # type: ignore[import-not-found] start_time = time.monotonic() telemetry_events.on_neutral_atom_trace() diff --git a/source/qdk_package/qdk/_device/_atom/_scheduler.py b/source/qdk_package/qdk/_device/_atom/_scheduler.py index 0ca1285e512..be178fbef6c 100644 --- a/source/qdk_package/qdk/_device/_atom/_scheduler.py +++ b/source/qdk_package/qdk/_device/_atom/_scheduler.py @@ -1,27 +1,31 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT License. -from ._utils import as_qis_gate, get_used_values, uses_any_value +from collections import defaultdict +from dataclasses import dataclass +from fractions import Fraction +from functools import lru_cache +from itertools import chain +from typing import Any, Iterable, Optional, TypeAlias + from pyqir import ( + BasicBlock, Call, - Instruction, Function, - QirModuleVisitor, FunctionType, + Instruction, + IntType, + Linkage, + Module, PointerType, + QirModuleVisitor, Type, - Linkage, - ptr_id, - IntType, Value, + ptr_id, ) + from .._device import Device, Zone, ZoneType -from collections import defaultdict -from dataclasses import dataclass -from itertools import chain -from typing import Iterable, TypeAlias, Optional -from fractions import Fraction -from functools import lru_cache +from ._utils import as_qis_gate, get_used_values, uses_any_value QubitId: TypeAlias = Value Location: TypeAlias = tuple[int, int] @@ -37,13 +41,13 @@ class Move: src_loc: Location dst_loc: Location - def __hash__(self): + def __hash__(self) -> int: return hash(self.qubit_id_ptr) - def __str__(self): + def __str__(self) -> str: return f"Move Qubit({self.qubit_id}): {self.src_loc} -> {self.dst_loc}" - def __repr__(self): + def __repr__(self) -> str: return self.__str__() @property @@ -52,10 +56,10 @@ def qubit_id(self) -> int: assert q_id is not None, "Qubit id should be known" return q_id - def parity(self): + def parity(self) -> tuple[int, int]: return move_parity(self.src_loc, self.dst_loc) - def direction(self): + def direction(self) -> tuple[int, int]: return move_direction(self.src_loc, self.dst_loc) @@ -116,11 +120,14 @@ def is_invalid_move_pair(move1: Move, move2: Move) -> bool: @lru_cache(maxsize=1 << 14) -def scale_factor_helper(source_diff, destination_diff): +def scale_factor_helper( + source_diff: int, destination_diff: int +) -> Optional[bool | Fraction]: if destination_diff == 0: return True if (s := Fraction(source_diff, destination_diff)) >= 0: return s + return None def scale_factor(move1: Move, move2: Move) -> Optional[MoveGroupScaleFactor]: @@ -142,6 +149,8 @@ def scale_factor(move1: Move, move2: Move) -> Optional[MoveGroupScaleFactor]: if row_scale_factor is not None and col_scale_factor is not None: return row_scale_factor, col_scale_factor + return None + class MoveGroup: """ @@ -156,7 +165,7 @@ class MoveGroup: __slots__ = ("moves", "scale_factor", "ref_move") - def __init__(self, moves: Iterable[Move]): + def __init__(self, moves: Iterable[Move]) -> None: self.moves = set(moves) self.scale_factor = scale_factor(*moves) if len(self.moves) > 1 else None self.ref_move = next(iter(moves)) @@ -164,7 +173,7 @@ def __init__(self, moves: Iterable[Move]): def __len__(self) -> int: return len(self.moves) - def add(self, move: Move): + def add(self, move: Move) -> None: """ Adds a move to this move group. @@ -174,15 +183,15 @@ def add(self, move: Move): # A move group with a single move doesn't have an associated scale factor. # Therefore, we cannot test if a move is compatible with it, which means # we cannot add moves to it. - assert ( - self.scale_factor - ), "cannot add to move group candidate with a single move" + assert self.scale_factor, ( + "cannot add to move group candidate with a single move" + ) self.moves.add(move) - def remove(self, move: Move): + def remove(self, move: Move) -> None: self.moves.remove(move) - def discard(self, move: Move): + def discard(self, move: Move) -> None: self.moves.discard(move) @@ -196,7 +205,7 @@ class MoveGroupPool: up/down and left/right direction of all moves in the pool. """ - def __init__(self): + def __init__(self) -> None: """Initializes a move-group pool for moves of the given ``parity`` and ``direction``. :param parity: The parity of source and destination columns of all the moves in this pool. @@ -224,7 +233,7 @@ def largest_move_group_candidate(self) -> Optional[MoveGroup]: except ValueError: return None - def add(self, move: Move): + def add(self, move: Move) -> None: """Adds a move to the move-group pool. :param move: The move to add. It must be of the same parity and direction as @@ -354,7 +363,7 @@ def __init__( device: Device, zone: Zone, qubits_to_move: list[QubitId | tuple[QubitId, QubitId]], - ): + ) -> None: """Initializes the move scheduler from a device, a target zone, and a list of qubits to move to that target zone. @@ -391,7 +400,7 @@ def build_zone_locations(self, zone: Zone) -> dict[Location, None]: def qubits_to_partial_moves( self, qubits_to_move: list[QubitId | tuple[QubitId, QubitId]] ) -> list[PartialMove | PartialMovePair]: - partial_moves = [] + partial_moves: list[PartialMove | PartialMovePair] = [] for elt in qubits_to_move: if isinstance(elt, tuple): q_id1 = ptr_id(elt[0]) @@ -407,7 +416,7 @@ def qubits_to_partial_moves( mov = PartialMove(elt, self.device.get_home_loc(q_id)) partial_moves.append(mov) - def sort_key(partial_move: PartialMove | PartialMovePair): + def sort_key(partial_move: PartialMove | PartialMovePair) -> int: if isinstance(partial_move, PartialMove): return self.device.get_ordering(partial_move.qubit_id) else: @@ -415,7 +424,7 @@ def sort_key(partial_move: PartialMove | PartialMovePair): return sorted(partial_moves, key=sort_key) - def is_empty(self): + def is_empty(self) -> bool: """ Returns `True` if all moves were scheduled. That is, there are no partial moves and all disjoint pools are empty. @@ -440,9 +449,10 @@ def add_to_largest_compatible_move_group( del self.available_dst_locations[move.dst_loc] return pool - if move := self.get_compatible_move(self.move_group_pool, partial_move): - self.move_group_pool.add(move) - del self.available_dst_locations[move.dst_loc] + compatible_move = self.get_compatible_move(self.move_group_pool, partial_move) + if compatible_move is not None: + self.move_group_pool.add(compatible_move) + del self.available_dst_locations[compatible_move.dst_loc] return self.move_group_pool raise Exception("not enough IZ space to schedule all moves") @@ -472,17 +482,18 @@ def add_pair_to_largest_compatible_move_group( del self.available_dst_locations[dst_loc2] return pool1 - if move1 := self.get_compatible_move( + compatible_move = self.get_compatible_move( self.move_group_pool, partial_move, is_pair=True - ): + ) + if compatible_move is not None: # Push the move corresponding to the first qubit of the CZ pair. - self.move_group_pool.add(move1) + self.move_group_pool.add(compatible_move) # Build the move corresponding to the second qubit of the CZ pair. - dest2 = (move1.dst_loc[0], move1.dst_loc[1] + 1) + dest2 = (compatible_move.dst_loc[0], compatible_move.dst_loc[1] + 1) move2 = partial_move_pair[1].into_move(dest2) self.move_group_pool.add(move2) - del self.available_dst_locations[move1.dst_loc] + del self.available_dst_locations[compatible_move.dst_loc] del self.available_dst_locations[move2.dst_loc] return self.move_group_pool raise Exception("not enough IZ space to schedule all moves") @@ -505,10 +516,12 @@ def get_destination( # We compute the destination row by solving this equation for `dst_row`: # src_row_diff / (group.ref_move.dst_loc[0] - dst_row) == row_scale_factor src_row_diff = group.ref_move.src_loc[0] - partial_move.src_loc[0] - dst_row = group.ref_move.dst_loc[0] - src_row_diff / row_scale_factor - assert isinstance(dst_row, Fraction) - if dst_row.denominator == 1: - dst_row = dst_row.numerator + dst_row_fraction = ( + group.ref_move.dst_loc[0] - src_row_diff / row_scale_factor + ) + assert isinstance(dst_row_fraction, Fraction) + if dst_row_fraction.denominator == 1: + dst_row = dst_row_fraction.numerator else: return None @@ -518,10 +531,12 @@ def get_destination( # We compute the destination col by solving this equation for `dst_col`: # src_col_diff / (group.ref_move.dst_loc[1] - dst_col) == col_scale_factor src_col_diff = group.ref_move.src_loc[1] - partial_move.src_loc[1] - dst_col = group.ref_move.dst_loc[1] - src_col_diff / col_scale_factor - assert isinstance(dst_col, Fraction) - if dst_col.denominator == 1: - dst_col = dst_col.numerator + dst_col_fraction = ( + group.ref_move.dst_loc[1] - src_col_diff / col_scale_factor + ) + assert isinstance(dst_col_fraction, Fraction) + if dst_col_fraction.denominator == 1: + dst_col = dst_col_fraction.numerator else: return None @@ -529,11 +544,13 @@ def get_destination( if loc in self.available_dst_locations: return loc + return None + def get_compatible_move( self, pool: MoveGroupPool, partial_move: PartialMove, - is_pair=False, + is_pair: bool = False, ) -> Optional[Move]: # First, try finding a large enough group to place the partial move in. if self.zone.type != ZoneType.MEAS: @@ -561,7 +578,9 @@ def get_compatible_move( if (not is_pair) or destination[1] % 2 == 0: return partial_move.into_move(destination) - def __iter__(self): + return None + + def __iter__(self) -> "MoveScheduler": return self def __next__(self) -> list[Move]: @@ -582,13 +601,13 @@ class Schedule(QirModuleVisitor): end_func: Function move_funcs: list[Function] - def __init__(self, device: Device): + def __init__(self, device: Device) -> None: super().__init__() self.device = device self.num_qubits = len(self.device.home_locs) self.pending_moves: list[list[Move]] = [] - def _on_module(self, module): + def _on_module(self, module: Module) -> None: i64_ty = IntType(module.context, 64) # Find or create the necessary runtime functions. for func in module.functions: @@ -628,7 +647,7 @@ def _on_module(self, module): super()._on_module(module) - def _on_block(self, block): + def _on_block(self, block: BasicBlock) -> None: # Use only the first interaction and measurement zone; more could be supported in future. interaction_zone = self.device.get_interaction_zones()[0] measurement_zone = self.device.get_measurement_zones()[0] @@ -636,21 +655,23 @@ def _on_block(self, block): max_measurements = self.device.column_count * measurement_zone.row_count # Track pending/queued single qubit operations by qubit id. - self.single_qubit_ops = [[] for _ in range(self.num_qubits)] + self.single_qubit_ops: list[list[tuple[Call, dict[str, Any]]]] = [ + [] for _ in range(self.num_qubits) + ] # Track pending CZ operations. - self.curr_cz_ops = [] + self.curr_cz_ops: list[Call] = [] # Track pending measurements. - self.measurements = [] + self.measurements: list[tuple[Call, dict[str, Any]]] = [] # Track pending qubits to move to an interaction or measurement zone. self.pending_qubits_to_move: list[QubitId | tuple[QubitId, QubitId]] = [] # Track values used in CZ ops and measurements to avoid putting operations on the # same qubit in the same batch. - self.vals_used_in_cz_ops = set() - self.vals_used_in_measurements = set() + self.vals_used_in_cz_ops: set[Value] = set() + self.vals_used_in_measurements: set[Value] = set() instructions = [instr for instr in block.instructions] for instr in instructions: @@ -743,23 +764,23 @@ def _on_block(self, block): while self.any_pending_ops(): self.flush_pending(instr) - def any_pending_single_qubit_ops(self): + def any_pending_single_qubit_ops(self) -> bool: return any(ops for ops in self.single_qubit_ops) - def any_pending_czs(self): + def any_pending_czs(self) -> bool: return bool(self.curr_cz_ops) - def any_pending_measurements(self): + def any_pending_measurements(self) -> bool: return bool(self.measurements) - def any_pending_ops(self): + def any_pending_ops(self) -> bool: return ( self.any_pending_czs() or self.any_pending_single_qubit_ops() or self.any_pending_measurements() ) - def flush_pending(self, insert_before: Instruction): + def flush_pending(self, insert_before: Instruction) -> None: interaction_zone = self.device.get_interaction_zones()[0] self.builder.insert_before(insert_before) # If cz ops pending, insert accumulated moves, single qubits ops matching cz rows, then the cz ops, then move back. @@ -793,7 +814,9 @@ def flush_pending(self, insert_before: Instruction): # insert those moves, then the ops, then move back. else: while self.any_pending_single_qubit_ops(): - target_qubits_by_row = [[] for _ in range(interaction_zone.row_count)] + target_qubits_by_row: list[list[int]] = [ + [] for _ in range(interaction_zone.row_count) + ] curr_row = 0 for q in range(self.num_qubits): if len(self.single_qubit_ops[q]) > 0: @@ -832,23 +855,23 @@ def target_qubits_by_row(self, zone: Zone) -> list[list[int]]: row.sort() return qubits_by_row - def schedule_pending_moves(self, zone: Zone): + def schedule_pending_moves(self, zone: Zone) -> None: move_scheduler = MoveScheduler(self.device, zone, self.pending_qubits_to_move) for move_group in move_scheduler: self.pending_moves.append(move_group) # self.verify_that_all_moves_were_scheduled() self.pending_qubits_to_move = [] - def verify_that_all_moves_were_scheduled(self): + def verify_that_all_moves_were_scheduled(self) -> None: moves_to_schedule = sum( len(x) if isinstance(x, tuple) else 1 for x in self.pending_qubits_to_move ) scheduled_moves = sum(len(group) for group in self.pending_moves) - assert ( - moves_to_schedule == scheduled_moves - ), f"{moves_to_schedule} != {scheduled_moves}" + assert moves_to_schedule == scheduled_moves, ( + f"{moves_to_schedule} != {scheduled_moves}" + ) - def insert_moves(self): + def insert_moves(self) -> None: """ For each pending move, insert a call to the move function that moves the given qubit to the given (row, col) location. @@ -877,7 +900,7 @@ def insert_moves(self): if move_group_id != 0: self.builder.call(self.end_func, []) - def insert_moves_back(self): + def insert_moves_back(self) -> None: move_group_id = 0 for move_group in self.pending_moves: # We can execute `MOVE_GROUPS_PER_PARALLEL_SECTION`, if @@ -905,7 +928,7 @@ def insert_moves_back(self): # Clear pending moves. self.pending_moves = [] - def flush_single_qubit_ops(self, target_qubits): + def flush_single_qubit_ops(self, target_qubits: list[int]) -> None: # Flush all pending single qubit ops for the given target qubits, combining # consecutive ops of the same type into a single parallel region by row in # the interaction zone. diff --git a/source/qdk_package/qdk/_device/_atom/_trace.py b/source/qdk_package/qdk/_device/_atom/_trace.py index 3308b525488..198e3329a4a 100644 --- a/source/qdk_package/qdk/_device/_atom/_trace.py +++ b/source/qdk_package/qdk/_device/_atom/_trace.py @@ -16,7 +16,7 @@ def __init__( "qubits": device.home_locs, "steps": [], } - self.q_cols = {} + self.q_cols: dict[int, int] = {} super().__init__() def _next_step(self): @@ -45,6 +45,7 @@ def _on_qis_move(self, call, qubit, row, col): if not self.in_parallel: self._next_step() q = ptr_id(qubit) + assert q is not None self.q_cols[q] = col.value self.trace["steps"][-1]["ops"].append(f"move({row.value}, {col.value}) {q}") @@ -52,12 +53,14 @@ def _on_qis_sx(self, call, qubit): if not self.in_parallel: self._next_step() q = ptr_id(qubit) + assert q is not None self.trace["steps"][-1]["ops"].append(f"sx {q}") def _on_qis_rz(self, call, angle, qubit): if not self.in_parallel: self._next_step() q = ptr_id(qubit) + assert q is not None self.trace["steps"][-1]["ops"].append(f"rz({angle.value}) {q}") def _on_qis_cz(self, call, qubit1, qubit2): @@ -65,6 +68,8 @@ def _on_qis_cz(self, call, qubit1, qubit2): self._next_step() q1 = ptr_id(qubit1) q2 = ptr_id(qubit2) + assert q1 is not None + assert q2 is not None if self.q_cols.get(q1, -1) > self.q_cols.get(q2, -1): q1, q2 = q2, q1 self.trace["steps"][-1]["ops"].append(f"cz {q1}, {q2}") diff --git a/source/qdk_package/qdk/_device/_device.py b/source/qdk_package/qdk/_device/_device.py index e0030fa18a2..19e46fb320f 100644 --- a/source/qdk_package/qdk/_device/_device.py +++ b/source/qdk_package/qdk/_device/_device.py @@ -45,7 +45,7 @@ def __init__(self, column_count: int, zones: list[Zone]): zone.set_offset(offset) offset += zone.row_count * self.column_count - self.home_locs = [] + self.home_locs: list[tuple[int, int]] = [] self._init_home_locs() def _init_home_locs(self): diff --git a/source/qdk_package/qdk/_interpreter.py b/source/qdk_package/qdk/_interpreter.py index 8324259256b..9319f5ab495 100644 --- a/source/qdk_package/qdk/_interpreter.py +++ b/source/qdk_package/qdk/_interpreter.py @@ -16,6 +16,7 @@ """ import warnings +import builtins from . import telemetry_events, code from ._native import ( # type: ignore Interpreter, @@ -181,7 +182,7 @@ def get_config() -> Config: return _get_default_context()._config -def python_args_to_interpreter_args(args): +def python_args_to_interpreter_args(args) -> tuple[Any, ...] | None: return _get_default_context()._python_args_to_interpreter_args(args) @@ -414,7 +415,7 @@ def _coerce_estimator_params( telemetry_events.on_estimate() start = monotonic() context = _get_default_context() - if isinstance(entry_expr, Callable) and hasattr(entry_expr, "__global_callable"): + if builtins.callable(entry_expr) and hasattr(entry_expr, "__global_callable"): args = context._python_args_to_interpreter_args(args) res_str = context._interpreter.estimate( param_str, callable=entry_expr.__global_callable, args=args @@ -524,7 +525,7 @@ def dump_operation(operation: str, num_qubits: int) -> List[List[complex]]: num_entries = pow(2, num_qubits) factor = math.sqrt(num_entries) ndigits = 6 - matrix = [] + matrix: list[list[complex]] = [] for i in range(num_entries): matrix += [[]] for j in range(num_entries): diff --git a/source/qdk_package/qdk/_ipython.py b/source/qdk_package/qdk/_ipython.py index d2140aa2e6c..f4e9688cd8d 100644 --- a/source/qdk_package/qdk/_ipython.py +++ b/source/qdk_package/qdk/_ipython.py @@ -9,8 +9,8 @@ """ from time import monotonic -from IPython.display import display, clear_output -from IPython.core.magic import register_cell_magic +from IPython.display import display, clear_output # type: ignore[import-not-found] +from IPython.core.magic import register_cell_magic # type: ignore[import-not-found] from ._native import QSharpError from ._interpreter import get_interpreter, qsharp_value_to_python_value from . import telemetry_events diff --git a/source/qdk_package/qdk/_native.pyi b/source/qdk_package/qdk/_native.pyi index 41e4be1bb3f..0df581e4b32 100644 --- a/source/qdk_package/qdk/_native.pyi +++ b/source/qdk_package/qdk/_native.pyi @@ -25,7 +25,7 @@ class OutputSemantics(Enum): and the semantic checks that are performed. """ - Qiskit: OutputSemantics + Qiskit = 0 """ The output is in Qiskit format meaning that the output is all of the classical registers, in reverse order @@ -33,7 +33,7 @@ class OutputSemantics(Enum): bit within each register in reverse order. """ - OpenQasm: OutputSemantics + OpenQasm = 1 """ [OpenQASM 3 has two output modes](https://openqasm.com/language/directives.html#input-output) - If the programmer provides one or more `output` declarations, then @@ -42,7 +42,7 @@ class OutputSemantics(Enum): - Otherwise, assume all of the declared variables are returned as output. """ - ResourceEstimation: OutputSemantics + ResourceEstimation = 2 """ No output semantics are applied. The entry point returns `Unit`. """ @@ -52,7 +52,7 @@ class ProgramType(Enum): Represents the type of compilation output to create """ - File: ProgramType + File = 0 """ Creates an operation in a namespace as if the program is a standalone file. Inputs are lifted to the operation params. Output are lifted to @@ -60,13 +60,13 @@ class ProgramType(Enum): as long as there are no input parameters. """ - Operation: ProgramType + Operation = 1 """ Programs are compiled to a standalone function. Inputs are lifted to the operation params. Output are lifted to the operation return type. """ - Fragments: ProgramType + Fragments = 2 """ Creates a list of statements from the program. This is useful for interactive environments where the program is a list of statements @@ -90,7 +90,7 @@ class TargetProfile(Enum): :raises ValueError: If the string does not match any target profile. """ - Base: TargetProfile + Base = 0 """ Target supports the minimal set of capabilities required to run a quantum program. @@ -98,7 +98,7 @@ class TargetProfile(Enum): This option maps to the Base Profile as defined by the QIR specification. """ - Adaptive_RI: TargetProfile + Adaptive_RI = 1 """ Target supports the Adaptive profile with the integer computation extension. @@ -107,7 +107,7 @@ class TargetProfile(Enum): extension defined by the QIR specification. """ - Adaptive_RIF: TargetProfile + Adaptive_RIF = 2 """ Target supports the Adaptive profile with integer & floating-point computation extensions. @@ -117,7 +117,7 @@ class TargetProfile(Enum): extension defined by the QIR specification. """ - Adaptive: TargetProfile + Adaptive = 3 """ Target supports the Adaptive profile with all supported extensions. @@ -125,7 +125,7 @@ class TargetProfile(Enum): all the optional extensions defined by the QIR specification. """ - Unrestricted: TargetProfile + Unrestricted = 4 """ Describes the unrestricted set of capabilities required to run any Q# program. """ @@ -368,7 +368,7 @@ class Interpreter: list_directory: Callable[[str], List[Dict[str, str]]], resolve_path: Callable[[str, str], str], fetch_github: Callable[[str, str, str, str], str], - **kwargs, + **kwargs: Any, ) -> Any: """ Imports OpenQASM source code into the active Q# interpreter. @@ -396,19 +396,19 @@ class Result(Enum): A Q# measurement result. """ - Zero: int - One: int - Loss: int + Zero = 0 + One = 1 + Loss = 2 class Pauli(Enum): """ A Q# Pauli operator. """ - I: int - X: int - Y: int - Z: int + I = 0 + X = 1 + Y = 2 + Z = 3 class Output: """ @@ -479,17 +479,17 @@ class CircuitGenerationMethod(Enum): The method to use for circuit generation. """ - ClassicalEval: CircuitGenerationMethod + ClassicalEval = 0 """ Use classical evaluation to generate the circuit. """ - Simulate: CircuitGenerationMethod + Simulate = 1 """ Use simulation to generate the circuit. """ - Static: CircuitGenerationMethod + Static = 2 """ Compile the program and transform to a circuit using partial evaluation. Only works for AdaptiveRIF-compliant programs. @@ -567,11 +567,12 @@ def compile_visual_circuit_to_qsharp( def circuit_qasm_program( source: str, + config: CircuitConfig, read_file: Callable[[str], Tuple[str, str]], list_directory: Callable[[str], List[Dict[str, str]]], resolve_path: Callable[[str, str], str], fetch_github: Callable[[str, str, str, str], str], - **kwargs, + **kwargs: Any, ) -> Circuit: """ Synthesizes a circuit for an OpenQASM program. @@ -603,7 +604,7 @@ def compile_qasm_program_to_qir( list_directory: Callable[[str], List[Dict[str, str]]], resolve_path: Callable[[str, str], str], fetch_github: Callable[[str, str, str, str], str], - **kwargs, + **kwargs: Any, ) -> str: """ Compiles the OpenQASM source code into a program that can be submitted to a @@ -638,7 +639,7 @@ def compile_qasm_to_qsharp( list_directory: Callable[[str], List[Dict[str, str]]], resolve_path: Callable[[str, str], str], fetch_github: Callable[[str, str, str, str], str], - **kwargs, + **kwargs: Any, ) -> str: """ Converts a OpenQASM program to Q#. @@ -685,7 +686,7 @@ def resource_estimate_qasm_program( list_directory: Callable[[str], List[Dict[str, str]]], resolve_path: Callable[[str, str], str], fetch_github: Callable[[str, str, str, str], str], - **kwargs, + **kwargs: Any, ) -> str: """ Estimates the resource requirements for executing OpenQASM source code. @@ -720,7 +721,7 @@ def run_qasm_program( list_directory: Callable[[str], List[Dict[str, str]]], resolve_path: Callable[[str, str], str], fetch_github: Callable[[str, str, str, str], str], - **kwargs, + **kwargs: Any, ) -> Any: """ Runs the given OpenQASM program for the given number of shots. @@ -756,9 +757,9 @@ def run_qasm_program( ... def estimate_custom( - algorithm, - qubit, - qec, + algorithm: Any, + qubit: dict, + qec: Any, factories: List = [], *, error_budget: float = 0.01, @@ -824,23 +825,23 @@ class TypeKind(Enum): A Q# type kind. """ - Primitive: int - Tuple: int - Array: int - Udt: int + Primitive = 0 + Tuple = 1 + Array = 2 + Udt = 3 class PrimitiveKind(Enum): """ A Q# primitive. """ - Bool: int - Int: int - Double: int - Complex: int - String: int - Pauli: int - Result: int + Bool = 0 + Int = 1 + Double = 2 + Complex = 3 + String = 4 + Pauli = 5 + Result = 6 class UdtIR: """ @@ -851,42 +852,42 @@ class UdtIR: fields: List[Tuple[str, TypeIR]] class QirInstructionId(Enum): - I: QirInstructionId - H: QirInstructionId - X: QirInstructionId - Y: QirInstructionId - Z: QirInstructionId - S: QirInstructionId - SAdj: QirInstructionId - SX: QirInstructionId - SXAdj: QirInstructionId - T: QirInstructionId - TAdj: QirInstructionId - CNOT: QirInstructionId - CX: QirInstructionId - CY: QirInstructionId - CZ: QirInstructionId - CCX: QirInstructionId - SWAP: QirInstructionId - RX: QirInstructionId - RY: QirInstructionId - RZ: QirInstructionId - RXX: QirInstructionId - RYY: QirInstructionId - RZZ: QirInstructionId - RESET: QirInstructionId - M: QirInstructionId - MResetZ: QirInstructionId - MZ: QirInstructionId - Move: QirInstructionId - ReadResult: QirInstructionId - ResultRecordOutput: QirInstructionId - BoolRecordOutput: QirInstructionId - IntRecordOutput: QirInstructionId - DoubleRecordOutput: QirInstructionId - TupleRecordOutput: QirInstructionId - ArrayRecordOutput: QirInstructionId - CorrelatedNoise: QirInstructionId + I = 0 + H = 1 + X = 2 + Y = 3 + Z = 4 + S = 5 + SAdj = 6 + SX = 7 + SXAdj = 8 + T = 9 + TAdj = 10 + CNOT = 11 + CX = 12 + CY = 13 + CZ = 14 + CCX = 15 + SWAP = 16 + RX = 17 + RY = 18 + RZ = 19 + RXX = 20 + RYY = 21 + RZZ = 22 + RESET = 23 + M = 24 + MResetZ = 25 + MZ = 26 + Move = 27 + ReadResult = 28 + ResultRecordOutput = 29 + BoolRecordOutput = 30 + IntRecordOutput = 31 + DoubleRecordOutput = 32 + TupleRecordOutput = 33 + ArrayRecordOutput = 34 + CorrelatedNoise = 35 class QirInstruction: ... @@ -916,7 +917,7 @@ class NoiseTable: for arbitrary pauli fields. """ - def __setattr__(self, name: str, value: float): + def __setattr__(self, name: str, value: float) -> None: """ Defining __setattr__ allows setting noise like this @@ -935,7 +936,7 @@ class NoiseTable: """ @overload - def set_pauli_noise(self, lst: list[tuple[str, float]]): + def set_pauli_noise(self, lst: list[tuple[str, float]]) -> None: """ The correlated pauli noise to use in simulation. Setting an element that was previously set overrides that entry with the new value. @@ -954,7 +955,7 @@ class NoiseTable: """ @overload - def set_pauli_noise(self, pauli_strings: list[str], values: list[float]): + def set_pauli_noise(self, pauli_strings: list[str], values: list[float]) -> None: """ The correlated pauli noise to use in simulation. Setting an element that was previously set overrides that entry with the new value. @@ -966,7 +967,7 @@ class NoiseTable: """ @overload - def set_pauli_noise(self, pauli_string: str, value: float): + def set_pauli_noise(self, pauli_string: str, value: float) -> None: """ The correlated pauli noise to use in simulation. Setting an element that was previously set overrides that entry with the new value. @@ -977,17 +978,17 @@ class NoiseTable: noise_table.set_pauli_noise("XZ", 1e-10) """ - def set_depolarizing(self, value: float): + def set_depolarizing(self, value: float) -> None: """ The depolarizing noise to use in simulation. """ - def set_bitflip(self, value: float): + def set_bitflip(self, value: float) -> None: """ The bit flip noise to use in simulation. """ - def set_phaseflip(self, value: float): + def set_phaseflip(self, value: float) -> None: """ The phase flip noise to use in simulation. """ @@ -1010,7 +1011,7 @@ class NoiseIntrinsicsTable: my_intrinsic_noise_table = noise_config.intrinsics["my_intrinsic"] """ - def __setitem__(self, name: str, value: float): + def __setitem__(self, name: str, value: float) -> None: """ Defining __setitem__ allows setting intrinsic noise tables like this: noise_config = NoiseConfig() @@ -1056,7 +1057,7 @@ class NoiseConfig: The noise table for a custom intrinsic. """ - def load_csv_dir(self, dir_path: str): + def load_csv_dir(self, dir_path: str) -> None: """ Loads noise tables from the specified directory path. For each .csv file found in the directory, the noise table is loaded and associated with a unique identifier. The name of the file (without the .csv extension) diff --git a/source/qdk_package/qdk/estimator/_estimator.py b/source/qdk_package/qdk/estimator/_estimator.py index d71b16f64a3..a2b288d636f 100644 --- a/source/qdk_package/qdk/estimator/_estimator.py +++ b/source/qdk_package/qdk/estimator/_estimator.py @@ -11,8 +11,8 @@ try: # Both markdown and mdx_math (from python-markdown-math) must be present for our markdown # rendering logic to work. If either is missing, we'll fall back to plain text. - import markdown - import mdx_math + import markdown # type: ignore[import-untyped] + import mdx_math # type: ignore[import-not-found] has_markdown = True except ImportError: @@ -138,7 +138,7 @@ def check_time(name, value): pat = r"^(\+?[0-9]*\.?[0-9]+([eE][-+]?[0-9]+)?)\s*(s|ms|μs|µs|us|ns)$" if re.match(pat, value) is None: raise ValueError( - f"{name} is not a valid time string; use a " "suffix s, ms, us, or ns" + f"{name} is not a valid time string; use a suffix s, ms, us, or ns" ) @@ -223,9 +223,7 @@ def post_validation(self, result): # instruction set must be set if self.instruction_set is None: - raise LookupError( - "instruction_set must be set for custom qubit " "parameters" - ) + raise LookupError("instruction_set must be set for custom qubit parameters") # NOTE at this point, we know that instruction set must have valid # value @@ -466,25 +464,23 @@ class EstimatorInputParamsItem: base class for batching via :class:`EstimatorParams`. """ - def __init__(self): + def __init__(self) -> None: super().__init__() self.qubit_params: EstimatorQubitParams = EstimatorQubitParams() self.qec_scheme: EstimatorQecScheme = EstimatorQecScheme() - self.distillation_unit_specifications = ( - [] - ) # type: List[DistillationUnitSpecification] + self.distillation_unit_specifications = [] # type: List[DistillationUnitSpecification] self.constraints: EstimatorConstraints = EstimatorConstraints() self.error_budget: Optional[Union[float, ErrorBudgetPartition]] = None self.estimate_type: Optional[str] = None - def as_dict(self, validate=True, additional_params=None) -> Dict[str, Any]: - result = {} + def as_dict(self, validate: bool = True, additional_params=None) -> Dict[str, Any]: + result: dict[str, Any] = {} qubit_params = self.qubit_params.as_dict(validate) if len(qubit_params) != 0: result["qubitParams"] = qubit_params - elif hasattr(additional_params, "qubit_params"): + elif additional_params and hasattr(additional_params, "qubit_params"): qubit_params = additional_params.qubit_params.as_dict(validate) if len(qubit_params) != 0: result["qubitParams"] = qubit_params @@ -492,7 +488,7 @@ def as_dict(self, validate=True, additional_params=None) -> Dict[str, Any]: qec_scheme = self.qec_scheme.as_dict(validate) if len(qec_scheme) != 0: result["qecScheme"] = qec_scheme - elif hasattr(additional_params, "qec_scheme"): + elif additional_params and hasattr(additional_params, "qec_scheme"): qec_scheme = additional_params.qec_scheme.as_dict(validate) if len(qec_scheme) != 0: result["qecScheme"] = qec_scheme @@ -504,8 +500,10 @@ def as_dict(self, validate=True, additional_params=None) -> Dict[str, Any]: result["distillationUnitSpecifications"] = [] result["distillationUnitSpecifications"].append(specification_dict) - if result.get("distillationUnitSpecifications") is not None and hasattr( - additional_params, "distillation_unit_specifications" + if ( + result.get("distillationUnitSpecifications") is not None + and additional_params is not None + and hasattr(additional_params, "distillation_unit_specifications") ): for specification in additional_params.distillation_unit_specifications: specification_dict = specification.as_dict(validate) @@ -518,7 +516,7 @@ def as_dict(self, validate=True, additional_params=None) -> Dict[str, Any]: constraints = self.constraints.as_dict(validate) if len(constraints) != 0: result["constraints"] = constraints - elif hasattr(additional_params, "constraints"): + elif additional_params and hasattr(additional_params, "constraints"): constraints = additional_params.constraints.as_dict(validate) if len(constraints) != 0: result["constraints"] = constraints @@ -533,7 +531,7 @@ def as_dict(self, validate=True, additional_params=None) -> Dict[str, Any]: result["errorBudget"] = self.error_budget elif isinstance(self.error_budget, ErrorBudgetPartition): result["errorBudget"] = self.error_budget.as_dict(validate) - elif hasattr(additional_params, "error_budget"): + elif additional_params and hasattr(additional_params, "error_budget"): if isinstance(additional_params.error_budget, float) or isinstance( additional_params.error_budget, int ): @@ -602,7 +600,7 @@ def items(self) -> List: "make_params with num_items parameter" ) - def as_dict(self, validate=True) -> Dict[str, Any]: + def as_dict(self, validate: bool = True, additional_params=None) -> Dict[str, Any]: """ Constructs a dictionary from the input params. @@ -646,11 +644,13 @@ class EstimatorResult(dict): def __init__(self, data: Union[Dict, List]): self._error = None + self._data: Union[Dict[Any, Any], List[Any]] if isinstance(data, list) and len(data) == 1: - data = data[0] - if not EstimatorResult._is_succeeded(data): - raise EstimatorError(data["code"], data["message"]) + single_result = data[0] + if not EstimatorResult._is_succeeded(single_result): + raise EstimatorError(single_result["code"], single_result["message"]) + data = single_result if isinstance(data, dict): self._data = data @@ -732,7 +732,7 @@ def _repr_html_(self): raise self._error return self._repr - def __getitem__(self, key): + def __getitem__(self, key: Any) -> Any: """ If the result represents a batching job and key is a slice, a side-by-side table comparison is shown for the indexes represented by @@ -753,7 +753,7 @@ def __getitem__(self, key): else: raise KeyError(key) - def _plot(self, **kwargs): + def _plot(self, **kwargs) -> None: """ Plots all result items in a space time plot, where the x-axis shows total runtime, and the y-axis shows total number of physical qubits. @@ -850,7 +850,7 @@ def _plot(self, **kwargs): plt.show() @property - def json(self): + def json(self) -> str: """ Returns a JSON representation of the resource estimation result data. """ @@ -915,9 +915,9 @@ def _item_result_table(self): md = markdown.Markdown(extensions=["mdx_math"]) for group in self["reportData"]["groups"]: html += f""" -
+
- {group['title']} + {group["title"]} """ for entry in group["entries"]: @@ -932,7 +932,7 @@ def _item_result_table(self): explanation = entry["explanation"] html += f""" - +
{entry['label']}{entry["label"]} {val} {entry["description"]} @@ -996,9 +996,9 @@ def _item_result_summary_table(self): md = markdown.Markdown(extensions=["mdx_math"]) for group in self["reportData"]["groups"]: html += f""" -
+
- {group['title']} + {group["title"]} """ for entry in group["entries"]: @@ -1011,7 +1011,7 @@ def _item_result_summary_table(self): explanation = entry["explanation"] html += f""" - + @@ -1047,9 +1047,9 @@ def _batch_result_table(self, indices): self[first_succeeded_item_index]["reportData"]["groups"] ): html += f""" -
+
- {group['title']} + {group["title"]}
{explanation}{entry['label']}{explanation}{entry["label"]} {val} {entry["description"]}
{item_headers}""" @@ -1099,10 +1099,6 @@ def _batch_result_table(self, indices): return html - @staticmethod - def _is_succeeded(obj): - return "status" in obj and obj["status"] == "success" - class EstimatorResultDiagram: def __init__(self, data): @@ -1163,7 +1159,7 @@ def json(self): return self._json def estimate( - self, params: Union[dict, List, EstimatorParams] = None + self, params: Union[dict, List, EstimatorParams, None] = None ) -> EstimatorResult: """ Estimates resources for the current logical counts, using the diff --git a/source/qdk_package/qdk/openqasm/_circuit.py b/source/qdk_package/qdk/openqasm/_circuit.py index 0312fa0c06f..51c97dc5ac6 100644 --- a/source/qdk_package/qdk/openqasm/_circuit.py +++ b/source/qdk_package/qdk/openqasm/_circuit.py @@ -2,6 +2,7 @@ # Licensed under the MIT License. from time import monotonic +import builtins from typing import Any, Callable, Dict, Optional, Union from .._fs import read_file, list_directory, resolve from .._http import fetch_github @@ -85,12 +86,13 @@ def circuit( prune_classical_qubits=prune_classical_qubits, ) - if isinstance(source, Callable) and hasattr(source, "__global_callable"): - args = python_args_to_interpreter_args(args) + if builtins.callable(source) and hasattr(source, "__global_callable"): res = get_interpreter().circuit( - config, callable=source.__global_callable, args=args + config, + callable=source.__global_callable, + args=python_args_to_interpreter_args(args), ) - else: + elif isinstance(source, str): # remove any entries from kwargs with a None key or None value kwargs = {k: v for k, v in kwargs.items() if k is not None and v is not None} @@ -106,6 +108,8 @@ def circuit( fetch_github, **kwargs, ) + else: + raise ValueError("source must be a QASM string or callable") durationMs = (monotonic() - start) * 1000 telemetry_events.on_circuit_qasm_end(durationMs) diff --git a/source/qdk_package/qdk/openqasm/_compile.py b/source/qdk_package/qdk/openqasm/_compile.py index 5eda8faaf52..249adeb799c 100644 --- a/source/qdk_package/qdk/openqasm/_compile.py +++ b/source/qdk_package/qdk/openqasm/_compile.py @@ -2,7 +2,8 @@ # Licensed under the MIT License. from time import monotonic -from typing import Any, Callable, Dict, Optional, Union +import builtins +from typing import Any, Callable, Union from .._fs import read_file, list_directory, resolve from .._http import fetch_github @@ -66,10 +67,11 @@ def compile( telemetry_events.on_compile_qasm(target_profile) - if isinstance(source, Callable) and hasattr(source, "__global_callable"): - args = python_args_to_interpreter_args(args) + if builtins.callable(source) and hasattr(source, "__global_callable"): ll_str = get_interpreter().qir( - entry_expr=None, callable=source.__global_callable, args=args + entry_expr=None, + callable=source.__global_callable, + args=python_args_to_interpreter_args(args), ) elif isinstance(source, str): # remove any entries from kwargs with a None key or None value diff --git a/source/qdk_package/qdk/openqasm/_estimate.py b/source/qdk_package/qdk/openqasm/_estimate.py index 047eef56885..a72438ab312 100644 --- a/source/qdk_package/qdk/openqasm/_estimate.py +++ b/source/qdk_package/qdk/openqasm/_estimate.py @@ -2,6 +2,7 @@ # Licensed under the MIT License. import json +import builtins import warnings from time import monotonic from typing import Any, Callable, Dict, List, Optional, Union, cast @@ -77,10 +78,12 @@ def _coerce_estimator_params( param_str = json.dumps(params) telemetry_events.on_estimate_qasm() start = monotonic() - if isinstance(source, Callable) and hasattr(source, "__global_callable"): - args = python_args_to_interpreter_args(args) + if builtins.callable(source) and hasattr(source, "__global_callable"): res_str = get_interpreter().estimate( - param_str, entry_expr=None, callable=source.__global_callable, args=args + param_str, + entry_expr=None, + callable=source.__global_callable, + args=python_args_to_interpreter_args(args), ) elif isinstance(source, str): # remove any entries from kwargs with a None key or None value diff --git a/source/qdk_package/qdk/openqasm/_ipython.py b/source/qdk_package/qdk/openqasm/_ipython.py index 32a11bf82b9..87212e6c761 100644 --- a/source/qdk_package/qdk/openqasm/_ipython.py +++ b/source/qdk_package/qdk/openqasm/_ipython.py @@ -5,7 +5,7 @@ _in_jupyter = False try: - from IPython.display import display + from IPython.display import display # type: ignore[import-not-found] if get_ipython().__class__.__name__ == "ZMQInteractiveShell": # type: ignore _in_jupyter = True # Jupyter notebook or qtconsole diff --git a/source/qdk_package/qdk/openqasm/_run.py b/source/qdk_package/qdk/openqasm/_run.py index 6567385db97..b7e873ff32b 100644 --- a/source/qdk_package/qdk/openqasm/_run.py +++ b/source/qdk_package/qdk/openqasm/_run.py @@ -2,6 +2,7 @@ # Licensed under the MIT License. from time import monotonic +import builtins from typing import Any, Callable, cast, Dict, List, Optional, Tuple, Union, Literal from .._fs import read_file, list_directory, resolve from .._http import fetch_github @@ -99,7 +100,7 @@ def run( ) start_time = monotonic() - results: List[ShotResult] = [] + results: List[Any] = [] def on_save_events(output: Output) -> None: # Append the output to the last shot's output list @@ -114,8 +115,7 @@ def on_save_events(output: Output) -> None: callable = None source_str: Optional[str] = None - if isinstance(source, Callable) and hasattr(source, "__global_callable"): - args = python_args_to_interpreter_args(args) + if builtins.callable(source) and hasattr(source, "__global_callable"): callable = source.__global_callable elif isinstance(source, str): source_str = source @@ -149,7 +149,7 @@ def on_save_events(output: Output) -> None: noise, qubit_loss=qubit_loss, callable=callable, - args=args, + args=python_args_to_interpreter_args(args), seed=kwargs.get("seed"), sim_type=type, num_qubits=num_qubits, @@ -211,6 +211,7 @@ def on_save_events(output: Output) -> None: if as_bitstring: from ._utils import as_bitstring as convert_to_bitstring - results = convert_to_bitstring(results) + converted_results: Any = convert_to_bitstring(results) + return converted_results return results diff --git a/source/qdk_package/qdk/qre/interop/_qir.py b/source/qdk_package/qdk/qre/interop/_qir.py index dd6d50eff4d..7cf1b400c6a 100644 --- a/source/qdk_package/qdk/qre/interop/_qir.py +++ b/source/qdk_package/qdk/qre/interop/_qir.py @@ -92,7 +92,7 @@ def trace_from_qir(input: str | bytes) -> Trace: else: mod = pyqir.Module.from_bitcode(context, input) - gates, num_qubits, _ = AggregateGatesPass().run(mod) + gates, num_qubits, _ = AggregateGatesPass().run_and_collect(mod) trace = Trace(compute_qubits=num_qubits) diff --git a/source/qdk_package/qdk/simulation/_simulation.py b/source/qdk_package/qdk/simulation/_simulation.py index 4aa6bf367d7..c2d6a55a976 100644 --- a/source/qdk_package/qdk/simulation/_simulation.py +++ b/source/qdk_package/qdk/simulation/_simulation.py @@ -3,7 +3,7 @@ from pathlib import Path import random -from typing import Callable, Literal, List, Optional, Tuple, TypeAlias, Union +from typing import Callable, Literal, List, Optional, Tuple, TypeAlias, Union, cast import pyqir from .._native import ( QirInstructionId, @@ -40,20 +40,19 @@ class AggregateGatesPass(pyqir.QirModuleVisitor): - def __init__(self): + def __init__(self) -> None: super().__init__() self.gates: List[QirInstruction | Tuple] = [] - self.required_num_qubits = None - self.required_num_results = None + self.required_num_qubits: Optional[int] = None + self.required_num_results: Optional[int] = None def _get_value_as_string(self, value: pyqir.Value) -> str: - value = pyqir.extract_byte_string(value) - if value is None: + value_bytes = pyqir.extract_byte_string(value) + if value_bytes is None: return "" - value = value.decode("utf-8") - return value + return value_bytes.decode("utf-8") - def run(self, mod: pyqir.Module) -> Tuple[List[QirInstruction | Tuple], int, int]: + def run(self, mod: pyqir.Module) -> None: errors = mod.verify() if errors is not None: raise ValueError(f"Module verification failed: {errors}") @@ -64,6 +63,13 @@ def run(self, mod: pyqir.Module) -> Tuple[List[QirInstruction | Tuple], int, int self.required_num_results = pyqir.required_num_results(func) super().run(mod) + + def run_and_collect( + self, mod: pyqir.Module + ) -> Tuple[List[QirInstruction | Tuple], int, int]: + self.run(mod) + assert self.required_num_qubits is not None + assert self.required_num_results is not None return (self.gates, self.required_num_qubits, self.required_num_results) def _on_block(self, block): @@ -121,52 +127,58 @@ def _on_call_instr(self, call: pyqir.Call) -> None: ) ) elif callee_name == "__quantum__qis__rx__body": + angle = cast(pyqir.FloatConstant, call.args[0]).value self.gates.append( ( QirInstructionId.RX, - call.args[0].value, + angle, pyqir.ptr_id(call.args[1]), ) ) elif callee_name == "__quantum__qis__rxx__body": + angle = cast(pyqir.FloatConstant, call.args[0]).value self.gates.append( ( QirInstructionId.RXX, - call.args[0].value, + angle, pyqir.ptr_id(call.args[1]), pyqir.ptr_id(call.args[2]), ) ) elif callee_name == "__quantum__qis__ry__body": + angle = cast(pyqir.FloatConstant, call.args[0]).value self.gates.append( ( QirInstructionId.RY, - call.args[0].value, + angle, pyqir.ptr_id(call.args[1]), ) ) elif callee_name == "__quantum__qis__ryy__body": + angle = cast(pyqir.FloatConstant, call.args[0]).value self.gates.append( ( QirInstructionId.RYY, - call.args[0].value, + angle, pyqir.ptr_id(call.args[1]), pyqir.ptr_id(call.args[2]), ) ) elif callee_name == "__quantum__qis__rz__body": + angle = cast(pyqir.FloatConstant, call.args[0]).value self.gates.append( ( QirInstructionId.RZ, - call.args[0].value, + angle, pyqir.ptr_id(call.args[1]), ) ) elif callee_name == "__quantum__qis__rzz__body": + angle = cast(pyqir.FloatConstant, call.args[0]).value self.gates.append( ( QirInstructionId.RZZ, - call.args[0].value, + angle, pyqir.ptr_id(call.args[1]), pyqir.ptr_id(call.args[2]), ) @@ -233,13 +245,23 @@ def _on_call_instr(self, call: pyqir.Call) -> None: ) elif callee_name == "__quantum__rt__tuple_record_output": tag = self._get_value_as_string(call.args[1]) + value = cast(pyqir.IntConstant, call.args[0]).value self.gates.append( - (QirInstructionId.TupleRecordOutput, str(call.args[0].value), tag) + ( + QirInstructionId.TupleRecordOutput, + str(value), + tag, + ) ) elif callee_name == "__quantum__rt__array_record_output": tag = self._get_value_as_string(call.args[1]) + value = cast(pyqir.IntConstant, call.args[0]).value self.gates.append( - (QirInstructionId.ArrayRecordOutput, str(call.args[0].value), tag) + ( + QirInstructionId.ArrayRecordOutput, + str(value), + tag, + ) ) elif ( callee_name == "__quantum__rt__initialize" @@ -247,7 +269,7 @@ def _on_call_instr(self, call: pyqir.Call) -> None: or callee_name == "__quantum__rt__end_parallel" or callee_name == "__quantum__qis__barrier__body" # We only hit this during noiseless simulations - or "qdk_noise" in call.callee.attributes.func + or "qdk_noise" in cast(pyqir.Function, call.callee).attributes.func ): pass else: @@ -274,7 +296,7 @@ def _on_call_instr(self, call: pyqir.Call) -> None: [pyqir.ptr_id(arg) for arg in call.args], ) ) - elif "qdk_noise" in call.callee.attributes.func: + elif "qdk_noise" in cast(pyqir.Function, call.callee).attributes.func: # If we are running a noisy simulation, we treat # missing noise intrinsics as an error. raise ValueError(f"Missing noise intrinsic: {callee_name}") @@ -305,7 +327,7 @@ def _on_call_instr(self, call: pyqir.Call) -> None: [pyqir.ptr_id(qubit) for qubit in call.args], # qubit args ) ) - elif "qdk_noise" in call.callee.attributes.func: + elif "qdk_noise" in cast(pyqir.Function, call.callee).attributes.func: # If we are running a noisy simulation, we treat # missing noise intrinsics as an error. raise ValueError(f"Missing noise intrinsic: {callee_name}") @@ -315,8 +337,8 @@ def _on_call_instr(self, call: pyqir.Call) -> None: class OutputRecordingPass(pyqir.QirModuleVisitor): _output_str = "" - _closers = [] - _counters = [] + _closers: List[str] = [] + _counters: List[int] = [] _process_fn = None def process_output(self, bitstring: str): @@ -367,7 +389,6 @@ def _on_rt_tuple_record_output(self, call, value, target): class DecomposeCcxPass(pyqir.QirModuleVisitor): - h_func: Function t_func: Function tadj_func: Function @@ -519,9 +540,9 @@ def run_base( Runs a base profile program given a rust simulator. Adds output recording logic. """ if noise is None: - gates, num_qubits, num_results = AggregateGatesPass().run(mod) + gates, num_qubits, num_results = AggregateGatesPass().run_and_collect(mod) else: - gates, num_qubits, num_results = CorrelatedNoisePass(noise).run(mod) + gates, num_qubits, num_results = CorrelatedNoisePass(noise).run_and_collect(mod) recorder = OutputRecordingPass() recorder.run(mod) return list( @@ -599,7 +620,7 @@ def run_qir_gpu( def prepare_qir_with_correlated_noise( input: Union[QirInputData, str, bytes], noise_tables: List[Tuple[int, str, int]], -) -> Tuple[List[QirInstruction], int, int]: +) -> Tuple[List[QirInstruction | Tuple], int, int]: # Turn the input into a QIR module mod, _, _, _ = preprocess_simulation_input(input, None, None, None) @@ -607,11 +628,7 @@ def prepare_qir_with_correlated_noise( DecomposeCcxPass().run(mod) # Extract the gates including correlated noise instructions - gates, required_num_qubits, required_num_results = GpuCorrelatedNoisePass( - noise_tables - ).run(mod) - - return (gates, required_num_qubits, required_num_results) + return GpuCorrelatedNoisePass(noise_tables).run_and_collect(mod) class GpuSimulator: diff --git a/source/qdk_package/qdk/telemetry.py b/source/qdk_package/qdk/telemetry.py index e1796278f6a..9051a05969e 100644 --- a/source/qdk_package/qdk/telemetry.py +++ b/source/qdk_package/qdk/telemetry.py @@ -252,7 +252,7 @@ def _post_telemetry() -> bool: # This is the thread that aggregates and posts telemetry at a regular interval. # The main thread will signal the thread loop to exit when the process is about to exit. -def _telemetry_thread_start(): +def _telemetry_thread_start() -> None: next_post_sec: Union[float, None] = None def on_metric(msg: Metric): diff --git a/source/qdk_package/qdk/test_utils.py b/source/qdk_package/qdk/test_utils.py index 02d83f86c75..0b0d7f4e292 100644 --- a/source/qdk_package/qdk/test_utils.py +++ b/source/qdk_package/qdk/test_utils.py @@ -2,9 +2,8 @@ from typing import Any -from qdk._interpreter import _get_context_or_default - from ._context import Context +from ._interpreter import _get_context_or_default def dump_operation_on_state( @@ -63,8 +62,8 @@ def dump_operation_on_state( initial_state, save_events=True, )[0] - state = result["events"][-1].state_dump().get_dict() - statevector = [0.0] * (2**num_qubits) + state: dict[int, complex] = result["events"][-1].state_dump().get_dict() + statevector = [0.0j] * (2**num_qubits) for index, amplitude in state.items(): statevector[index] = amplitude return statevector diff --git a/source/qdk_package/qdk/widgets.py b/source/qdk_package/qdk/widgets.py index 041385e2dee..3ba4f327cba 100644 --- a/source/qdk_package/qdk/widgets.py +++ b/source/qdk_package/qdk/widgets.py @@ -8,7 +8,7 @@ """ try: - from qsharp_widgets import * # pyright: ignore[reportWildcardImportFromLibrary] + from qsharp_widgets import * # type: ignore[import-not-found] except Exception as ex: raise ImportError( "qdk.widgets requires the jupyter extra. Install with 'pip install qdk[jupyter]'." diff --git a/source/widgets/src/qsharp_widgets/__init__.py b/source/widgets/src/qsharp_widgets/__init__.py index 5f5e172c7ea..c01c7c65dfc 100644 --- a/source/widgets/src/qsharp_widgets/__init__.py +++ b/source/widgets/src/qsharp_widgets/__init__.py @@ -6,8 +6,8 @@ import time from typing import Literal -import anywidget -import traitlets +import anywidget # type: ignore +import traitlets # type: ignore try: @@ -321,7 +321,7 @@ def __init__( """ if wavefunction is not None: try: - from qdk_chemistry.data import Wavefunction as _Wavefunction + from qdk_chemistry.data import Wavefunction as _Wavefunction # type: ignore except ImportError: raise ImportError( "The 'qdk-chemistry' package is required when passing a "
Item