diff --git a/glue/stimside/src/stimside/op_handlers/leakage_handlers/common_parsing.py b/glue/stimside/src/stimside/op_handlers/leakage_handlers/common_parsing.py new file mode 100644 index 000000000..8e805bfd8 --- /dev/null +++ b/glue/stimside/src/stimside/op_handlers/leakage_handlers/common_parsing.py @@ -0,0 +1,67 @@ +import re +from typing import Literal + +# --- REGEXES --- + +# Matches "LEAKAGE_NAME: (arg1) (arg2)" +LEAKAGE_TAG_MATCH = re.compile(r"(?PLEAKAGE_\w+):(?P.+)") + +# Matches "CONDITIONED_ON_NAME: args : targets" +CONDITION_TAG_MATCH = re.compile( + r"(?PCONDITIONED_ON_[A-Z]+)\s*:(?P[^:]+):*(?P.+)*" +) + +# Matches "LEAKAGE_MEASUREMENT: (prob, state), ... : target, ..." +MEASURE_TAG_MATCH = re.compile(r"(?PLEAKAGE_MEASUREMENT)\s*:(?P.+):(?P.+)") + +# Matches "state-->state" or "state<->state" +# Allows multi-digit states and 'U' +LEAKAGE_TRANSITIONS_1_MATCH = re.compile( + r"(?P[\dU]+)(?P[-<]->)(?P[\dU]+)" +) + +# Matches "state_state-->state_state" +# Allows states: digits, U, V, D, X, Y, Z +LEAKAGE_TRANSITIONS_2_MATCH = re.compile( + r"(?P[\dU])_(?P[\dU])(?P[-<]->)(?P[\dUVDXYZ])_(?P[\dUVDXYZ])" +) + + +# --- UTILITIES --- + +def try_as_integer(state: str) -> int | str: + """Try to parse a state as an integer, otherwise return the stripped string.""" + s = state.strip() + if s.isdigit(): + return int(s) + return s + + +def split_arguments(args: str) -> list[tuple[str, ...]]: + """ + Robustly split parenthesized arguments, e.g., "(0.1, 2) (0.2, 3)" -> [("0.1", "2"), ("0.2", "3")]. + Handles optional commas and varied whitespace between parentheses. + """ + stripped_args = args.strip() + if not stripped_args: + raise ValueError("Empty arguments") + + if not (stripped_args.startswith("(") and stripped_args.endswith(")")): + raise ValueError(f"Arguments must be enclosed in parentheses: '{args}'") + + # re.split(r"\)\s*,*\s*\(", ...) handles ") (", "), (", ") ,(", etc. + args_list = re.split(r"\)\s*,*\s*\(", stripped_args[1:-1]) + return [tuple(b.strip() for b in a.split(",")) for a in args_list] + + +def parse_list_of_targets(targets: str) -> list[int]: + """Parse a space-separated list of qubit targets.""" + targets_set = [] + for target in targets.split(): + if target.isdigit(): + targets_set.append(int(target)) + else: + raise ValueError( + f"Targets in tag must be integers separated by spaces, got: '{targets}'" + ) + return targets_set diff --git a/glue/stimside/src/stimside/op_handlers/leakage_handlers/leakage_tag_parsing_flip.py b/glue/stimside/src/stimside/op_handlers/leakage_handlers/leakage_tag_parsing_flip.py index 12d634557..1f9c013aa 100644 --- a/glue/stimside/src/stimside/op_handlers/leakage_handlers/leakage_tag_parsing_flip.py +++ b/glue/stimside/src/stimside/op_handlers/leakage_handlers/leakage_tag_parsing_flip.py @@ -1,360 +1,17 @@ -import re -from typing import Literal, cast - import stim # type: ignore[import-untyped] -from stimside.op_handlers.leakage_handlers.leakage_parameters import ( - LeakageControlledErrorParams, - LeakageParams, - LeakageTransition1Params, - LeakageTransition2Params, - LeakageTransitionZParams, - LeakageMeasurementParams -) - -LEAKAGE_TAG_MATCH = re.compile(r"(?PLEAKAGE_\w+):(?P.+)") -# you can use this by doing -# match = LEAKAGE_TAG_MATCH.fullmatch(tag) -# the result is None if the tag isn't formatted correctly -# otherwise the result has named groups: -# match.group('name') -# match.group('args') - -MEASURE_TAG_MATCH = re.compile(r"LEAKAGE_MEASUREMENT\s*:(?P.+):(?P.+)") -## "MPAD[LEAKAGE_MEASUREMENT: (0.1, 0) (0.2, 1) (0.9, 2) (0.99, 3) : 1 3 5 7]" - -LEAKAGE_TRANSITIONS_1_MATCH = re.compile( - r"(?P[\dU]+)(?P[-<]->)(?P[\dU]+)" -) -LEAKAGE_TRANSITIONS_2_MATCH = re.compile( - r"(?P[\dU])_(?P[\dU])(?P[-<]->)(?P[\dUV])_(?P[\dUV])" -) - - -def _parse_leakage_controlled_error( - args_tuples: list[tuple[str, ...]], tag: str -) -> LeakageControlledErrorParams: - errors: list[tuple[float, int, Literal["X", "Y", "Z"]]] = [] - for a in args_tuples: - if len(a) != 2: - raise ValueError( - f"Malformed parsed argument '{a}' in tag '{tag}'. " - f"Arguments should be comma separated and in the form " - "'(probability, leakage_state --> pauli)'" - ) - a_parsed = [a[0]] + [s.strip() for s in a[1].split("-->")] - if len(a_parsed) != 3: - raise ValueError( - f"Malformed parsed argument {a_parsed} in {tag}. " - f"Arguments should be comma separated and in the form " - "(probability, leakage_state --> pauli)" - ) - p = float(a_parsed[0]) - state = try_as_integer(a_parsed[1]) - if not isinstance(state, int) or state <= 1: - raise ValueError(f"State argument must be a leakage state >=2, got {state}") - pauli = a_parsed[2] - errors.append((p, state, cast(Literal["X", "Y", "Z"], pauli))) - return LeakageControlledErrorParams(args=tuple(errors), from_tag=tag) - - -def _parse_leakage_transition_1_or_z( - args_tuples: list[tuple[str, ...]], tag: str -) -> LeakageTransition1Params | LeakageTransitionZParams: - name = tag.split(":")[0] - transitions = [] - for a in args_tuples: - if len(a) != 2: - raise ValueError( - f"Malformed parsed argument {a} in {tag}. " - f"Arguments should be comma separated and in the form " - "probability, leakage_transitions" - ) - p = float(a[0]) - this_transition = a[1] - match = LEAKAGE_TRANSITIONS_1_MATCH.fullmatch(this_transition) - if not match: # the tag failed the regex: - raise ValueError( - f"Malformed {name} argument '{a[1]}' in tag '{tag}'. " - f"The transitions arguments should match the form " - f"'int_first_state{{-->,<->}}int_second_state'" - ) - s0 = try_as_integer(match.group("s0")) - s1 = try_as_integer(match.group("s1")) - - transitions.append((p, s0, s1)) - - if match.group("direction") == "<->": - transitions.append((p, s1, s0)) - - if tag.startswith("LEAKAGE_TRANSITION_1"): - return LeakageTransition1Params( - args=cast( - tuple[tuple[float, int | Literal["U"], int | Literal["U"]]], - tuple(transitions), - ), - from_tag=tag, - ) - else: - return LeakageTransitionZParams( - args=cast(tuple[tuple[float, int, int]], tuple(transitions)), from_tag=tag - ) - - -def _parse_leakage_transition_2( - args_tuples: list[tuple[str, ...]], tag: str -) -> LeakageTransition2Params: - transitions = [] - for a in args_tuples: - if len(a) != 2: - raise ValueError( - f"Malformed parsed argument {a} in {tag}. " - f"Arguments should be comma separated and in the form " - "probability:leakage_transitions" - ) - p = float(a[0]) - this_transition = a[1] - match = LEAKAGE_TRANSITIONS_2_MATCH.fullmatch(this_transition) - if not match: # the tag failed the regex: - raise ValueError( - f"Malformed LEAKAGE_TRANSITIONS_2 transitions argument '{a}' in tag '{tag}'. " - f"The transitions arguments should match the form " - f"'state_state{{-->,<->}}state_state'" - ) - - s0 = try_as_integer(match.group("s0")) - s1 = try_as_integer(match.group("s1")) - s2 = try_as_integer(match.group("s2")) - s3 = try_as_integer(match.group("s3")) - - transitions.append((p, (s0, s1), (s2, s3))) - - if match.group("direction") == "<->": - transitions.append((p, (s2, s3), (s0, s1))) - - return LeakageTransition2Params( - args=cast( - tuple[ - tuple[ - float, - tuple[int | Literal["U"], int | Literal["U"]], - tuple[ - Literal["U", "V", "X", "Y", "Z", "D"] | int, - Literal["U", "V", "X", "Y", "Z", "D"] | int, - ], - ] - ], - tuple(transitions), - ), - from_tag=tag, - ) - -def _parse_leakage_projection_z( - args_tuples: list[tuple[str, ...]], tag: str -) -> LeakageMeasurementParams: - projections = [] - for a in args_tuples: - if len(a) != 2: - raise ValueError( - f"Malformed parsed argument '{a}' in tag '{tag}'. " - f"Arguments should be comma separated and in the form " - "'(probability, state)'" - ) - p = float(a[0]) - state = int(a[1]) - projections.append((p, state)) - return LeakageMeasurementParams(args=tuple(projections), targets=None, from_tag=tag) - -def _parse_list_of_targets(targets: str) -> list[int]: - """Parse a space-separated list of qubit targets.""" - targets_set = [] - for target in targets.split(): - if target.isdigit(): - targets_set += [int(target)] - else: - raise ValueError( - "Targets in tag are not integers separated" f" by spaces: {targets}" - ) - return targets_set - -def _parse_leakage_measurement(tag: str) -> LeakageMeasurementParams: - """ - Parse a LEAKAGE_MEASUREMENT tag. - For example: - "MPAD[LEAKAGE_MEASUREMENT: (0.1, 0) (0.2, 1) (0.9, 2) (0.99, 3) : 1 3 5 7]" - """ - match = MEASURE_TAG_MATCH.fullmatch(tag) - if match is None: - raise ValueError( - "To use the leakage measurement tag, use the following syntax:" - '"LEAKAGE_MEASUREMENT: (prob, state), ... : target, ...".' - ) - - args = match.group("args") - target_args = match.group("targets") - - projections: list[tuple[float, int]] = [] - if args: - stripped_args = args.strip() - if not stripped_args: - raise ValueError(f"Empty arguments in tag '{tag}'") - if not (stripped_args.startswith("(") and stripped_args.endswith(")")): - raise ValueError( - f"Arguments must be enclosed in parentheses in tag '{tag}'" - ) - # Strip outer parens and split by ') (' - # re.split(r"\)\s*,*\s*\(", stripped_args[1:-1]) - args_list = stripped_args[1:-1].split(") (") - args_tuples = [tuple(b.strip() for b in a.split(",")) for a in args_list] - - for a in args_tuples: - if len(a) != 2: - raise ValueError( - f"Malformed parsed argument '{a}' in tag '{tag}'. " - f"Arguments should be comma separated and in the form " - "'(probability, state)'" - ) - p = float(a[0]) - state = try_as_integer(a[1]) - if isinstance(state, str): - raise ValueError( - f"State {state} in tag '{tag}' is not a valid integer." - ) - else: - projections.append((p, state)) - - targets = _parse_list_of_targets(target_args) - return LeakageMeasurementParams( - args=tuple(projections), targets=tuple(targets), from_tag=tag - ) - -TAG_PARSERS = { - "LEAKAGE_CONTROLLED_ERROR": _parse_leakage_controlled_error, - "LEAKAGE_TRANSITION_1": _parse_leakage_transition_1_or_z, - "LEAKAGE_TRANSITION_Z": _parse_leakage_transition_1_or_z, - "LEAKAGE_TRANSITION_2": _parse_leakage_transition_2, - "LEAKAGE_PROJECTION_Z": _parse_leakage_projection_z, -} +from stimside.op_handlers.leakage_handlers.leakage_parameters import LeakageParams +from stimside.op_handlers.leakage_handlers.tag_registry import parse_leakage_tag as _unified_parse_tag +from stimside.op_handlers.leakage_handlers.tag_registry import _parse_leakage_in_circuit_recurse as _unified_parse_circuit def parse_leakage_tag(op: stim.CircuitInstruction) -> LeakageParams | None: - """parse the string leakage tag to extract the included numbers. - - args: - op: the stim.CircuitInstruction to parse the tag for - - returns: - a LeakageParameters object corresponding to the parsed instruction - - raises: - Value errors if: - the leakage tag name is unrecognised - """ - gd = stim.gate_data(op.name) - tag = op.tag - - # start unpacking - if it doesn't start with LEAKAGE, given up immediately - if not tag.startswith("LEAKAGE"): - return None - - if tag.startswith("LEAKAGE_MEASUREMENT"): - if op.name != "MPAD": - raise ValueError("Only MPAD can have a LEAKAGE_MEASUREMENT tag.") - return _parse_leakage_measurement(tag) - - # from here on out, we raise an error on anything malformed - match = LEAKAGE_TAG_MATCH.fullmatch(tag) - if match is None: # the tag failed the regex: - raise ValueError( - f"Malformed LEAKAGE tag {tag}. " - "If a tag begins with LEAKAGE, we demand it match the pattern " - "LEAKAGE_NAME: (arg) (arg) ..." - ) - name = match.group("name") - args = match.group("args") - - args_tuples = [] - if args: - stripped_args = args.strip() - if not stripped_args: - raise ValueError(f"Empty arguments in tag '{tag}'") - if not (stripped_args.startswith("(") and stripped_args.endswith(")")): - raise ValueError( - f"Arguments must be enclosed in parentheses in tag '{tag}'" - ) - # Strip outer parens and split by ') (' - args_list = stripped_args[1:-1].split(") (") - args_tuples = [tuple(b.strip() for b in a.split(",")) for a in args_list] - - # Check tag is attached to a reasonable gate - # first check qubit-arity - if ( - name in ["LEAKAGE_TRANSITION_1", "LEAKAGE_TRANSITION_Z", "LEAKAGE_PROJECTION_Z"] - and not gd.is_single_qubit_gate - ): - raise ValueError( - f"1Q leakage tag {op.tag} attached to not-1Q stim gate {op.name}. " - ) - if ( - name in ["LEAKAGE_CONTROLLED_ERROR", "LEAKAGE_TRANSITION_2"] - and not gd.is_two_qubit_gate - ): - raise ValueError( - f"2Q leakage tag {op.tag} attached to not-2Q stim gate {op.name}. " - ) - - # then check specific gate attachment - if name == "LEAKAGE_PROJECTION_Z": - if op.name != "M": - raise ValueError(f"LEAKAGE_PROJECTION_Z must be attached to an M gate") - elif name == "LEAKAGE_TRANSITION_Z": - if op.name not in ["I", "I_ERROR", "R"]: - raise ValueError( - f"LEAKAGE_TRANSITION_Z must be attached to an I, I_ERROR or R gate" - ) - elif op.name not in ["I", "I_ERROR", "II", "II_ERROR"]: - raise ValueError( - f"Leakage tag {name} must be attached to a trivially acting gate " - f"(I, I_ERROR, II, II_ERROR)" - ) - - if name in TAG_PARSERS: - return TAG_PARSERS[name](args_tuples, tag) - - if name in TAG_PARSERS: - raise ValueError( - f"Failed to recognise existing leakage tag name {name}. " - "This one is on us, not you. File a bug." - ) - raise ValueError( - f"Unrecognised LEAKAGE tag name {name}: " - f"must be one of {list(TAG_PARSERS.keys())}" - ) + """Parse a leakage tag for the flip simulator.""" + return _unified_parse_tag(op, simulator="flip") def parse_leakage_in_circuit( circuit: stim.Circuit, ) -> dict[stim.CircuitInstruction, LeakageParams]: """Parse all present leakage tags in a circuit, including inside repeats.""" - return _parse_leakage_in_circuit_recurse(circuit=circuit) - - -def _parse_leakage_in_circuit_recurse( - circuit: stim.Circuit, -) -> dict[stim.CircuitInstruction, LeakageParams]: - parsed_tags = {} - for op in circuit: - if type(op) == stim.CircuitRepeatBlock: - parsed_tags.update(_parse_leakage_in_circuit_recurse(op.body_copy())) - elif op.tag != "": - parsed = parse_leakage_tag(op) - if parsed is not None: - parsed_tags[op] = parsed - - return parsed_tags - - -def try_as_integer(state: str) -> int | str: - if state.isdigit(): - return int(state) - return state + return _unified_parse_circuit(circuit, simulator="flip") diff --git a/glue/stimside/src/stimside/op_handlers/leakage_handlers/leakage_tag_parsing_flip_test.py b/glue/stimside/src/stimside/op_handlers/leakage_handlers/leakage_tag_parsing_flip_test.py index ce3b11249..6f7a1fb20 100644 --- a/glue/stimside/src/stimside/op_handlers/leakage_handlers/leakage_tag_parsing_flip_test.py +++ b/glue/stimside/src/stimside/op_handlers/leakage_handlers/leakage_tag_parsing_flip_test.py @@ -9,6 +9,7 @@ LeakageTransitionZParams, LeakageMeasurementParams ) +from stimside.op_handlers.leakage_handlers.common_parsing import try_as_integer # ############################################################################## # Tests for successful parsing @@ -73,7 +74,7 @@ def test_bad_tags(): I_ERROR[LEAKAGE MALFORMED TAG] """ ) - with pytest.raises(ValueError, match="Malformed LEAKAGE tag"): + with pytest.raises(ValueError, match="Malformed leakage tag structure"): ltp.parse_leakage_tag(circuit[0]) circuit = stim.Circuit( @@ -81,7 +82,7 @@ def test_bad_tags(): I_ERROR[LEAKAGE_UNRECOGNIZED_TAG: (arg)] """ ) - with pytest.raises(ValueError, match="Unrecognised LEAKAGE tag"): + with pytest.raises(ValueError, match="Unrecognised leakage tag name"): ltp.parse_leakage_tag(circuit[0]) circuit = stim.Circuit( @@ -97,7 +98,7 @@ def test_bad_tags(): II_ERROR[LEAKAGE_CONTROLLED_ERROR: (BAD_ARG, BAD_ARG) ] """ ) - with pytest.raises(ValueError, match="Malformed parsed argument"): + with pytest.raises(ValueError, match="Malformed transition"): ltp.parse_leakage_tag(circuit[0]) circuit = stim.Circuit( @@ -105,7 +106,7 @@ def test_bad_tags(): I_ERROR[LEAKAGE_TRANSITION_1: (0.1, 1-->2, 3)] 0 """ ) - with pytest.raises(ValueError, match="Malformed parsed argument"): + with pytest.raises(ValueError, match="Malformed argument"): ltp.parse_leakage_tag(circuit[0]) circuit = stim.Circuit( @@ -113,7 +114,7 @@ def test_bad_tags(): I_ERROR[LEAKAGE_TRANSITION_1: (0.1, 1->2)] 0 """ ) - with pytest.raises(ValueError, match="Malformed LEAKAGE_TRANSITION_1 argument"): + with pytest.raises(ValueError, match="Malformed transition"): ltp.parse_leakage_tag(circuit[0]) circuit = stim.Circuit( @@ -121,7 +122,7 @@ def test_bad_tags(): II_ERROR[LEAKAGE_TRANSITION_2: (0.1, 1_1-->2_0, 3)] 0 1 """ ) - with pytest.raises(ValueError, match="Malformed parsed argument"): + with pytest.raises(ValueError, match="Malformed argument"): ltp.parse_leakage_tag(circuit[0]) circuit = stim.Circuit( @@ -129,7 +130,7 @@ def test_bad_tags(): II_ERROR[LEAKAGE_TRANSITION_2: (0.1, 1_1->2_0)] 0 1 """ ) - with pytest.raises(ValueError, match="Malformed LEAKAGE_TRANSITIONS_2 transitions argument"): + with pytest.raises(ValueError, match="Malformed transition"): ltp.parse_leakage_tag(circuit[0]) circuit = stim.Circuit( @@ -137,7 +138,7 @@ def test_bad_tags(): II_ERROR[LEAKAGE_CONTROLLED_ERROR: (0.1, 1, Z)] 0 1 """ ) - with pytest.raises(ValueError, match="Malformed parsed argument"): + with pytest.raises(ValueError, match="Malformed argument"): ltp.parse_leakage_tag(circuit[0]) circuit = stim.Circuit( @@ -223,5 +224,5 @@ def test_parse_in_circuit_nested_repeats(): def test_try_as_integer(): """Tests the try_as_integer helper function.""" - assert ltp.try_as_integer("2") == 2 - assert ltp.try_as_integer("U") == "U" + assert try_as_integer("2") == 2 + assert try_as_integer("U") == "U" diff --git a/glue/stimside/src/stimside/op_handlers/leakage_handlers/leakage_tag_parsing_tableau.py b/glue/stimside/src/stimside/op_handlers/leakage_handlers/leakage_tag_parsing_tableau.py index c05736b1f..613f8f6e0 100644 --- a/glue/stimside/src/stimside/op_handlers/leakage_handlers/leakage_tag_parsing_tableau.py +++ b/glue/stimside/src/stimside/op_handlers/leakage_handlers/leakage_tag_parsing_tableau.py @@ -1,506 +1,17 @@ -from typing import cast, Literal -import re - import stim # type: ignore[import-untyped] -from stimside.op_handlers.leakage_handlers.leakage_parameters import ( - LeakageParams, - LeakageTransition1Params, - LeakageTransition2Params, - LeakageConditioningParams, - LeakageMeasurementParams, -) - -CONDITION_TAG_MATCH = re.compile( - r"(?PCONDITIONED_ON_[A-Z]+)\s*:(?P[^:]+):*(?P.+)*" -) -## Conditional tag. Specififies if qubit is in certain states, do op otherwise do nothing -## Examples: -## "CONDITIONED_ON_SELF: U 2 3" : 1 qubit op -## DEPOLARIZE1[CONDITIONED_ON_OTHER: U 2 3: 10 12 14 16](0.1) 9 11 13 15 : 1 qubit op -## "CONDITIONED_ON_PAIR: (2, 3) (U, 2) (U, 3)" : 2 qubit op -## Can condition on U, L, or specific state - -MEASURE_TAG_MATCH = re.compile(r"LEAKAGE_MEASUREMENT\s*:(?P.+):(?P.+)") -## "MPAD[LEAKAGE_MEASUREMENT: (0.1, 0) (0.2, 1) (0.9, 2) (0.99, 3) : 1 3 5 7]" - -LEAKAGE_TAG_MATCH = re.compile(r"(?PLEAKAGE_\w+):(?P.+)") -# you can use this by doing -# match = LEAKAGE_TAG_MATCH.fullmatch(tag) -# the result is None if the tag isn't formatted correctly -# otherwise the result has named groups: -# match.group('name') -# match.group('args') - -LEAKAGE_TRANSITIONS_1_MATCH = re.compile( - r"(?P[\dU])(?P[-<]->)(?P[\dU])" -) -LEAKAGE_TRANSITIONS_2_MATCH = re.compile( - r"(?P[\dU])_(?P[\dU])(?P[-<]->)(?P[\dUVDXYZ])_(?P[\dUVDXYZ])" -) - - -def _parse_leakage_transition_1( - args_tuples: list[tuple[str, ...]], tag: str -) -> LeakageTransition1Params: - """Parse a LEAKAGE_TRANSITION_1""" - name = tag.split(":")[0] - transitions = [] - for a in args_tuples: - if len(a) != 2: - raise ValueError( - f"Malformed parsed argument {a} in {tag}. " - f"Arguments should be comma separated and in the form " - "probability, leakage_transitions" - ) - p = float(a[0]) - this_transition = a[1] - match = LEAKAGE_TRANSITIONS_1_MATCH.fullmatch(this_transition) - if not match: # the tag failed the regex: - raise ValueError( - f"Malformed {name} argument '{a[1]}' in tag '{tag}'. " - f"The transitions arguments should match the form " - f"'first_state{{-->,<->}}second_state'" - ) - s0 = try_as_integer(match.group("s0")) - s1 = try_as_integer(match.group("s1")) - - transitions.append((p, s0, s1)) - - if match.group("direction") == "<->": - transitions.append((p, s1, s0)) - - return LeakageTransition1Params( - args=cast( - tuple[tuple[float, int | Literal["U"], int | Literal["U"]]], - tuple(transitions), - ), - from_tag=tag, - ) - - -def _parse_leakage_transition_2( - args_tuples: list[tuple[str, ...]], tag: str -) -> LeakageTransition2Params: - """Parse a LEAKAGE_TRANSITION_2 tag.""" - transitions = [] - for a in args_tuples: - if len(a) != 2: - raise ValueError( - f"Malformed parsed argument {a} in {tag}. " - f"Arguments should be comma separated and in the form " - "probability:leakage_transitions" - ) - p = float(a[0]) - this_transition = a[1] - match = LEAKAGE_TRANSITIONS_2_MATCH.fullmatch(this_transition) - if not match: # the tag failed the regex: - raise ValueError( - f"Malformed LEAKAGE_TRANSITIONS_2 transitions argument '{a}' in tag '{tag}'. " - f"The transitions arguments should match the form " - f"'state_state{{-->,<->}}state_state'" - ) - - s0 = try_as_integer(match.group("s0")) - s1 = try_as_integer(match.group("s1")) - s2 = try_as_integer(match.group("s2")) - s3 = try_as_integer(match.group("s3")) - - transitions.append((p, (s0, s1), (s2, s3))) - - if match.group("direction") == "<->": - transitions.append((p, (s2, s3), (s0, s1))) - - return LeakageTransition2Params( - args=cast( - tuple[ - tuple[ - float, - tuple[int | Literal["U"], int | Literal["U"]], - tuple[ - Literal["U", "V", "X", "Y", "Z", "D"] | int, - Literal["U", "V", "X", "Y", "Z", "D"] | int, - ], - ] - ], - tuple(transitions), - ), - from_tag=tag, - ) - -def _parse_leakage_projection_z( - args_tuples: list[tuple[str, ...]], tag: str -) -> LeakageMeasurementParams: - """Parse a LEAKAGE_PROJECTION_Z tag.""" - projections = [] - for a in args_tuples: - if len(a) != 2: - raise ValueError( - f"Malformed parsed argument '{a}' in tag '{tag}'. " - f"Arguments should be comma separated and in the form " - "'(probability, state)'" - ) - p = float(a[0]) - state = int(a[1]) - projections.append((p, state)) - return LeakageMeasurementParams(args=tuple(projections), targets=None, from_tag=tag) - -def _parse_leakage_measurement(tag: str) -> LeakageMeasurementParams: - """ - Parse a LEAKAGE_MEASUREMENT tag. - For example: - "MPAD[LEAKAGE_MEASUREMENT: (0.1, 0) (0.2, 1) (0.9, 2) (0.99, 3) : 1 3 5 7]" - """ - match = MEASURE_TAG_MATCH.fullmatch(tag) - if match is None: - raise ValueError( - "To use the leakage measurement tag, use the following syntax:" - '"LEAKAGE_MEASUREMENT: (prob, state), ... : target, ...".' - ) - - args = match.group("args") - target_args = match.group("targets") - - projections: list[tuple[float, int]] = [] - if args: - stripped_args = args.strip() - if not stripped_args: - raise ValueError(f"Empty arguments in tag '{tag}'") - if not (stripped_args.startswith("(") and stripped_args.endswith(")")): - raise ValueError( - f"Arguments must be enclosed in parentheses in tag '{tag}'" - ) - # Strip outer parens and split by ') (' - # re.split(r"\)\s*,*\s*\(", stripped_args[1:-1]) - args_list = stripped_args[1:-1].split(") (") - args_tuples = [tuple(b.strip() for b in a.split(",")) for a in args_list] - - for a in args_tuples: - if len(a) != 2: - raise ValueError( - f"Malformed parsed argument '{a}' in tag '{tag}'. " - f"Arguments should be comma separated and in the form " - "'(probability, state)'" - ) - p = float(a[0]) - state = try_as_integer(a[1]) - if isinstance(state, str): - raise ValueError( - f"State {state} in tag '{tag}' is not a valid integer." - ) - else: - projections.append((p, state)) - - targets = _parse_list_of_targets(target_args) - return LeakageMeasurementParams( - args=tuple(projections), targets=tuple(targets), from_tag=tag - ) - -def _combine_conditioned_states(states: list[int | str]) -> list[int | str]: - """ - Combine a list of conditioned states into a minimal representation. - For example, [0, 1, 2, 'U'] -> ['U', 2] - or [0, 1, 2, 2] -> [2, 'U'] - Only valid input states are integers 0-9 and 'U'. - """ - if "U" in states: - new_states: list[int | str] = ["U"] - for st in states: - if isinstance(st, int): - if st > 1 and st < 10: - new_states.append(st) - elif st < 0 or st > 9: - raise ValueError( - f"Qubit state {st} can not be negative or larger than 9." - ) - elif st != "U": - raise ValueError( - "The conditioned state list in _combine_conditioned_states has an element" - "that is not U or an integer." - ) - return new_states - else: - new_states = [] - qubit_states = [] - for st in states: - assert isinstance(st, int), ( - "The conditioned state list in _combine_conditioned_states has an element" - + "that is not U or an integer." - ) - if st > 1 and st < 10: - new_states.append(st) - elif st in [0, 1]: - qubit_states.append(st) - else: - raise ValueError( - f"Qubit state {st} can not be negative or larger than 9." - ) - if 0 in qubit_states and 1 in qubit_states: - new_states.append("U") - else: - new_states += qubit_states - - return new_states - - -def _parse_list_of_states(args: str) -> set[str | int]: - """Parse a space-separated list of states, combining them appropriately.""" - states = [] - for st in args.split(): - states.append(try_as_integer(st.strip())) - return set(_combine_conditioned_states(states)) - - -def _parse_list_of_targets(targets: str) -> list[int]: - """Parse a space-separated list of qubit targets.""" - targets_set = [] - for target in targets.split(): - if target.isdigit(): - targets_set += [int(target)] - else: - raise ValueError( - "Targets in tag are not integers separated" f" by spaces: {targets}" - ) - return targets_set - - -def _parse_conditioned_on_self(args: str, tag: str) -> LeakageConditioningParams: - """Parse a CONDITIONED_ON_SELF tag.""" - states = _parse_list_of_states(args) - return LeakageConditioningParams(args=(tuple(states),), from_tag=tag, targets=None) - - -def _parse_conditioned_on_other( - args: str, targets: str, tag: str -) -> LeakageConditioningParams: - """Parse a CONDITIONED_ON_OTHER tag.""" - states = _parse_list_of_states(args) - - target_list = _parse_list_of_targets(targets) - return LeakageConditioningParams( - args=(tuple(states),), from_tag=tag, targets=tuple(target_list) - ) - - -def _parse_conditioned_on_pair(args: str, tag: str) -> LeakageConditioningParams: - """Parse a CONDITIONED_ON_PAIR tag.""" - states: list[list[int | str]] = [[], []] - for arg in (args.strip())[1:-1].split(") ("): - state_pair = arg.strip().split(",") - if len(state_pair) != 2: - raise ValueError( - f"The argument in a CONDITIONED_ON_PAIR tag {tag} does not have exactly" - "two qubit states prescribed in a paranthesis." - ) - state_pair_converted: list[int | str] = [ - try_as_integer(state) for state in state_pair - ] - for state in state_pair_converted: - if isinstance(state, str) and state not in ["U"]: - raise ValueError( - f"In tag {tag}, the state {state} conditioned on is not valid." - "It can only be an integer," - "or the letter U as an unleaked state." - ) - states[0].append(state_pair_converted[0]) - states[1].append(state_pair_converted[1]) - - return LeakageConditioningParams( - args=(tuple(states[0]), tuple(states[1])), from_tag=tag, targets=None - ) - -LEAKAGE_TAG_PARSERS = { - "LEAKAGE_TRANSITION_1": _parse_leakage_transition_1, - "LEAKAGE_TRANSITION_2": _parse_leakage_transition_2, - "LEAKAGE_PROJECTION_Z": _parse_leakage_projection_z, -} - -CONDITION_TAG_PARSERS = [ - "CONDITIONED_ON_SELF", - "CONDITIONED_ON_OTHER", - "CONDITIONED_ON_PAIR", -] - -ADDITIONAL_TAGS = [ - "LEAKAGE_MEASUREMENT", -] +from stimside.op_handlers.leakage_handlers.leakage_parameters import LeakageParams +from stimside.op_handlers.leakage_handlers.tag_registry import parse_leakage_tag as _unified_parse_tag +from stimside.op_handlers.leakage_handlers.tag_registry import _parse_leakage_in_circuit_recurse as _unified_parse_circuit def parse_leakage_tag(op: stim.CircuitInstruction) -> LeakageParams | None: - """parse the string leakage tag to extract the included numbers. - - args: - op: the stim.CircuitInstruction to parse the tag for - - returns: - a LeakageParameters object corresponding to the parsed instruction - - raises: - Value errors if: - the leakage tag name is unrecognised - """ - - gd = stim.gate_data(op.name) - tag = op.tag - - if tag.startswith("CONDITIONED_ON"): - match = CONDITION_TAG_MATCH.fullmatch(tag) - if match is None: - raise ValueError( - f"Malformed CONDITIONED_ON tag {tag}. " - "If a tag begins with CONDITIONED_ON, we demand it match one of the patterns " - '"CONDITIONED_ON_SELF: state ..." or ' - '"CONDITIONED_ON_OTHER: state ... : target ..." or ' - '"CONDITIONED_ON_PAIR: (p, state) ..."' - ) - name = match.group("name") - - if ( - name in ["CONDITIONED_ON_SELF", "CONDITIONED_ON_OTHER"] - and not gd.is_single_qubit_gate - ): - raise ValueError( - f"1Q condition tag {op.tag} attached to not-1Q stim gate {op.name}. " - ) - elif name in ["CONDITIONED_ON_PAIR"] and not gd.is_two_qubit_gate: - raise ValueError( - f"2Q condition tag {op.tag} attached to not-2Q stim gate {op.name}. " - ) - - if name in ["CONDITIONED_ON_SELF"]: - args = match.group("args") - return _parse_conditioned_on_self(args, tag) - elif name in ["CONDITIONED_ON_PAIR"]: - args = match.group("args") - return _parse_conditioned_on_pair(args, tag) - elif name in ["CONDITIONED_ON_OTHER"]: - args = match.group("args") - targets = match.group("targets") - if targets: - return _parse_conditioned_on_other(args, targets, tag) - else: - raise ValueError( - "CONDITIONED_ON_OTHER tag needs to specify targets. Please follow the pattern" - '"CONDITIONED_ON_OTHER: state ... : target ..."' - ) - else: - raise ValueError( - f"CONDITIONED_ON tags can only be one of {CONDITION_TAG_PARSERS}" - ) - - # if it doesn't start with LEAKAGE, given up - if not tag.startswith("LEAKAGE"): - return None - - if tag.startswith("LEAKAGE_MEASUREMENT"): - if op.name != "MPAD": - raise ValueError("Only MPAD can have a LEAKAGE_MEASUREMENT tag.") - return _parse_leakage_measurement(tag) - - # from here on out, we raise an error on anything malformed - match = LEAKAGE_TAG_MATCH.fullmatch(tag) - if match is None: # the tag failed the regex: - raise ValueError( - f"Malformed LEAKAGE tag {tag}. " - "If a tag begins with LEAKAGE, we demand it match the pattern " - "LEAKAGE_NAME: (arg) (arg) ..." - ) - name = match.group("name") - args = match.group("args") - - args_tuples = [] - if args: - stripped_args = args.strip() - if not stripped_args: - raise ValueError(f"Empty arguments in tag '{tag}'") - if not (stripped_args.startswith("(") and stripped_args.endswith(")")): - raise ValueError( - f"Arguments must be enclosed in parentheses in tag '{tag}'" - ) - # Strip outer parens and split by ') (' - args_list = re.split(r"\)\s*,*\s*\(", stripped_args[1:-1]) # .split(") (") - args_tuples = [tuple(b.strip() for b in a.split(",")) for a in args_list] - - # Check tag is attached to a reasonable gate - # first check qubit-arity - if ( - name in ["LEAKAGE_TRANSITION_1", "LEAKAGE_PROJECTION_Z"] - and not gd.is_single_qubit_gate - ): - raise ValueError( - f"1Q leakage tag {op.tag} attached to not-1Q stim gate {op.name}. " - ) - if ( - name in ["LEAKAGE_TRANSITION_2"] - and not gd.is_two_qubit_gate - ): - raise ValueError( - f"2Q leakage tag {op.tag} attached to not-2Q stim gate {op.name}. " - ) - - # then check specific gate attachment - if name == "LEAKAGE_PROJECTION_Z": - if op.name != "M": - raise ValueError(f"LEAKAGE_PROJECTION_Z must be attached to an M gate") - elif name == "LEAKAGE_TRANSITION_Z": - raise ValueError( - f"LEAKAGE_TRANSITION_Z is not used in the tableau simulator." - " Use LEAKAGE_TRANSITION_1 instead." - ) - elif name == "LEAKAGE_CONTROLLED_ERROR": - raise ValueError( - f"LEAKAGE_CONTROLLED_ERROR is not used in the tableau simulator." - " Use CONDITIONED_ON_* instead." - ) - elif op.name not in ["I", "I_ERROR", "II", "II_ERROR"]: - raise ValueError( - f"Leakage tag {name} must be attached to a trivially acting gate " - f"(I, I_ERROR, II, II_ERROR)" - ) - - if name in LEAKAGE_TAG_PARSERS: - return LEAKAGE_TAG_PARSERS[name](args_tuples, tag) - - if name in LEAKAGE_TAG_PARSERS: - raise ValueError( - f"Failed to recognise existing leakage tag name {name}. " - "This one is on us, not you. File a bug." - ) - raise ValueError( - f"Unrecognised LEAKAGE tag name {name}: " - f"must be one of { - list(LEAKAGE_TAG_PARSERS.keys()) + - ADDITIONAL_TAGS - }" - ) + """Parse a leakage tag for the tableau simulator.""" + return _unified_parse_tag(op, simulator="tableau") def parse_leakage_in_circuit( circuit: stim.Circuit, ) -> dict[stim.CircuitInstruction, LeakageParams]: """Parse all present leakage tags in a circuit, including inside repeats.""" - return _parse_leakage_in_circuit_recurse(circuit=circuit) - - -def _parse_leakage_in_circuit_recurse( - circuit: stim.Circuit, -) -> dict[stim.CircuitInstruction, LeakageParams]: - """Helper function to parse all present leakage tags in a circuit, including inside repeats.""" - parsed_tags = {} - for op in circuit: - if type(op) == stim.CircuitRepeatBlock: - parsed_tags.update(_parse_leakage_in_circuit_recurse(op.body_copy())) - - elif op.tag != "": - parsed = parse_leakage_tag(op) - if parsed is not None: - parsed_tags[op] = parsed - - return parsed_tags - - -def try_as_integer(state: str) -> int | str: - """Try to parse a state as an integer, otherwise return the stripped string.""" - if state.isdigit(): - return int(state) - return state.strip() + return _unified_parse_circuit(circuit, simulator="tableau") diff --git a/glue/stimside/src/stimside/op_handlers/leakage_handlers/leakage_tag_parsing_tableau_test.py b/glue/stimside/src/stimside/op_handlers/leakage_handlers/leakage_tag_parsing_tableau_test.py index d14dde204..b47d7789b 100644 --- a/glue/stimside/src/stimside/op_handlers/leakage_handlers/leakage_tag_parsing_tableau_test.py +++ b/glue/stimside/src/stimside/op_handlers/leakage_handlers/leakage_tag_parsing_tableau_test.py @@ -8,6 +8,7 @@ LeakageConditioningParams, LeakageMeasurementParams ) +from stimside.op_handlers.leakage_handlers.common_parsing import try_as_integer # ############################################################################## # Tests for successful parsing @@ -80,7 +81,7 @@ def test_bad_tags(): I_ERROR[LEAKAGE MALFORMED TAG] """ ) - with pytest.raises(ValueError, match="Malformed LEAKAGE tag"): + with pytest.raises(ValueError, match="Malformed leakage tag structure"): ltp.parse_leakage_tag(circuit[0]) circuit = stim.Circuit( @@ -88,7 +89,7 @@ def test_bad_tags(): I_ERROR[LEAKAGE_UNRECOGNIZED_TAG: (arg)] """ ) - with pytest.raises(ValueError, match="Unrecognised LEAKAGE tag"): + with pytest.raises(ValueError, match="Unrecognised leakage tag name"): ltp.parse_leakage_tag(circuit[0]) circuit = stim.Circuit( @@ -96,7 +97,7 @@ def test_bad_tags(): II_ERROR[LEAKAGE_CONTROLLED_ERROR: (0.1, 2-->X) (0.2, 3-->Z)] 0 1 2 3 """ ) - with pytest.raises(ValueError, match="Use CONDITIONED_ON"): + with pytest.raises(ValueError, match="is not supported in the"): ltp.parse_leakage_tag(circuit[0]) circuit = stim.Circuit( @@ -104,7 +105,7 @@ def test_bad_tags(): I_ERROR[LEAKAGE_TRANSITION_1: (0.1, 1-->2, 3)] 0 """ ) - with pytest.raises(ValueError, match="Malformed parsed argument"): + with pytest.raises(ValueError, match="Malformed argument"): ltp.parse_leakage_tag(circuit[0]) circuit = stim.Circuit( @@ -112,7 +113,7 @@ def test_bad_tags(): I_ERROR[LEAKAGE_TRANSITION_1: (0.1, 1->2)] 0 """ ) - with pytest.raises(ValueError, match="Malformed LEAKAGE_TRANSITION_1 argument"): + with pytest.raises(ValueError, match="Malformed transition"): ltp.parse_leakage_tag(circuit[0]) circuit = stim.Circuit( @@ -120,7 +121,7 @@ def test_bad_tags(): II_ERROR[LEAKAGE_TRANSITION_2: (0.1, 1_1-->2_0, 3)] 0 1 """ ) - with pytest.raises(ValueError, match="Malformed parsed argument"): + with pytest.raises(ValueError, match="Malformed argument"): ltp.parse_leakage_tag(circuit[0]) circuit = stim.Circuit( @@ -128,7 +129,7 @@ def test_bad_tags(): II_ERROR[LEAKAGE_TRANSITION_2: (0.1, 1_1->2_0)] 0 1 """ ) - with pytest.raises(ValueError, match="Malformed LEAKAGE_TRANSITIONS_2 transitions argument"): + with pytest.raises(ValueError, match="Malformed transition"): ltp.parse_leakage_tag(circuit[0]) """Tests that leakage tags are attached to gates of the correct arity.""" @@ -199,5 +200,5 @@ def test_parse_in_circuit_nested_repeats(): def test_try_as_integer(): """Tests the try_as_integer helper function.""" - assert ltp.try_as_integer("2") == 2 - assert ltp.try_as_integer("U") == "U" + assert try_as_integer("2") == 2 + assert try_as_integer("U") == "U" diff --git a/glue/stimside/src/stimside/op_handlers/leakage_handlers/leakage_tag_parsing_unified.py b/glue/stimside/src/stimside/op_handlers/leakage_handlers/leakage_tag_parsing_unified.py new file mode 100644 index 000000000..66ac6d970 --- /dev/null +++ b/glue/stimside/src/stimside/op_handlers/leakage_handlers/leakage_tag_parsing_unified.py @@ -0,0 +1,138 @@ +import stim # type: ignore[import-untyped] +from typing import Literal, Any, cast + +from stimside.op_handlers.leakage_handlers.common_parsing import ( + LEAKAGE_TRANSITIONS_1_MATCH, + LEAKAGE_TRANSITIONS_2_MATCH, + try_as_integer, + split_arguments, + parse_list_of_targets, +) + +from stimside.op_handlers.leakage_handlers.leakage_parameters import ( + LeakageTransition1Params, + LeakageTransition2Params, + LeakageTransitionZParams, + LeakageConditioningParams, + LeakageMeasurementParams, + LeakageControlledErrorParams, +) + +# --- PARSER HELPERS --- + +def _combine_conditioned_states(states: list[int | str]) -> list[int | str]: + if "U" in states: + new_states: list[int | str] = ["U"] + for st in states: + if isinstance(st, int): + if 1 < st < 10: new_states.append(st) + elif st < 0 or st > 9: raise ValueError(f"State {st} out of range 0-9.") + elif st != "U": raise ValueError("Invalid state element.") + return new_states + else: + new_states, qubit_states = [], [] + for st in states: + if 1 < st < 10: new_states.append(st) + elif st in [0, 1]: qubit_states.append(st) + else: raise ValueError(f"State {st} out of range 0-9.") + if 0 in qubit_states and 1 in qubit_states: new_states.append("U") + else: new_states += qubit_states + return new_states + +def _parse_list_of_states(args: str) -> set[str | int]: + states = [try_as_integer(st.strip()) for st in args.split()] + return set(_combine_conditioned_states(cast(list[int | str], states))) + +# --- TAG-SPECIFIC PARSER FUNCTIONS --- + +def _parse_controlled_error(op: stim.CircuitInstruction, match: Any, sim: str) -> LeakageControlledErrorParams: + args_tuples = split_arguments(match.group("args")) + errors = [] + for a in args_tuples: + if len(a) != 2: raise ValueError(f"Malformed argument '{a}' in tag '{op.tag}'.") + a_parsed = [a[0]] + [s.strip() for s in a[1].split("-->")] + if len(a_parsed) != 3: raise ValueError(f"Malformed transition '{a_parsed}' in {op.tag}.") + p, state, pauli = float(a_parsed[0]), try_as_integer(a_parsed[1]), a_parsed[2] + if not isinstance(state, int) or state <= 1: raise ValueError(f"State must be >= 2, got {state}") + errors.append((p, state, cast(Literal["X", "Y", "Z"], pauli))) + return LeakageControlledErrorParams(args=tuple(errors), from_tag=op.tag) + +def _parse_transition_1(op: stim.CircuitInstruction, match: Any, sim: str) -> LeakageTransition1Params: + args_tuples = split_arguments(match.group("args")) + transitions = [] + for a in args_tuples: + if len(a) != 2: raise ValueError(f"Malformed argument '{a}' in tag '{op.tag}'.") + p = float(a[0]) + m = LEAKAGE_TRANSITIONS_1_MATCH.fullmatch(a[1]) + if not m: raise ValueError(f"Malformed transition '{a[1]}' in tag '{op.tag}'.") + s0, s1 = try_as_integer(m.group("s0")), try_as_integer(m.group("s1")) + transitions.append((p, s0, s1)) + if m.group("direction") == "<->": transitions.append((p, s1, s0)) + return LeakageTransition1Params(args=cast(Any, tuple(transitions)), from_tag=op.tag) + +def _parse_transition_z(op: stim.CircuitInstruction, match: Any, sim: str) -> LeakageTransitionZParams: + # Use transition 1 logic but return Z params + res = _parse_transition_1(op, match, sim) + return LeakageTransitionZParams(args=cast(Any, res.args), from_tag=op.tag) + +def _parse_transition_2(op: stim.CircuitInstruction, match: Any, sim: str) -> LeakageTransition2Params: + args_tuples = split_arguments(match.group("args")) + transitions = [] + for a in args_tuples: + if len(a) != 2: raise ValueError(f"Malformed argument '{a}' in tag '{op.tag}'.") + p = float(a[0]) + m = LEAKAGE_TRANSITIONS_2_MATCH.fullmatch(a[1]) + if not m: raise ValueError(f"Malformed transition '{a[1]}' in tag '{op.tag}'.") + s0, s1, s2, s3 = (try_as_integer(m.group(g)) for g in ["s0", "s1", "s2", "s3"]) + transitions.append((p, (s0, s1), (s2, s3))) + if m.group("direction") == "<->": transitions.append((p, (s2, s3), (s0, s1))) + return LeakageTransition2Params(args=cast(Any, tuple(transitions)), from_tag=op.tag) + +def _parse_projection_z(op: stim.CircuitInstruction, match: Any, sim: str) -> LeakageMeasurementParams: + args_tuples = split_arguments(match.group("args")) + projections = [] + for a in args_tuples: + if len(a) != 2: raise ValueError(f"Malformed argument '{a}' in tag '{op.tag}'.") + projections.append((float(a[0]), int(a[1]))) + return LeakageMeasurementParams(args=tuple(projections), targets=None, from_tag=op.tag) + +def _parse_measurement(op: stim.CircuitInstruction, match: Any, sim: str) -> LeakageMeasurementParams: + args, target_args = match.group("args"), match.group("targets") + projections = [] + if args: + for a in split_arguments(args): + p, state = float(a[0]), try_as_integer(a[1]) + if isinstance(state, str): raise ValueError(f"State {state} is not an integer.") + projections.append((p, state)) + return LeakageMeasurementParams( + args=tuple(projections), + targets=tuple(parse_list_of_targets(target_args)), + from_tag=op.tag + ) + +def _parse_conditioned_self(op: stim.CircuitInstruction, match: Any, sim: str) -> LeakageConditioningParams: + states = _parse_list_of_states(match.group("args")) + return LeakageConditioningParams(args=(tuple(states),), from_tag=op.tag, targets=None) + +def _parse_conditioned_other(op: stim.CircuitInstruction, match: Any, sim: str) -> LeakageConditioningParams: + args, targets = match.group("args"), match.group("targets") + if not targets: raise ValueError("CONDITIONED_ON_OTHER tag needs to specify targets.") + states = _parse_list_of_states(args) + return LeakageConditioningParams( + args=(tuple(states),), + from_tag=op.tag, + targets=tuple(parse_list_of_targets(targets)) + ) + +def _parse_conditioned_pair(op: stim.CircuitInstruction, match: Any, sim: str) -> LeakageConditioningParams: + states: list[list[int | str]] = [[], []] + for pair in split_arguments(match.group("args")): + if len(pair) != 2: raise ValueError(f"CONDITIONED_ON_PAIR must have pairs of states.") + converted = [try_as_integer(s) for s in pair] + for s in converted: + if isinstance(s, str) and s != "U": raise ValueError(f"Invalid state {s}.") + states[0].append(converted[0]) + states[1].append(converted[1]) + return LeakageConditioningParams(args=(tuple(states[0]), tuple(states[1])), from_tag=op.tag, targets=None) + + diff --git a/glue/stimside/src/stimside/op_handlers/leakage_handlers/tag_registry.py b/glue/stimside/src/stimside/op_handlers/leakage_handlers/tag_registry.py new file mode 100644 index 000000000..37f2543ad --- /dev/null +++ b/glue/stimside/src/stimside/op_handlers/leakage_handlers/tag_registry.py @@ -0,0 +1,174 @@ +from dataclasses import dataclass, field +from typing import Callable, Literal, Any, Sequence +import stim # type: ignore[import-untyped] +import stimside.op_handlers.leakage_handlers.leakage_tag_parsing_unified as ltp + +from stimside.op_handlers.leakage_handlers.common_parsing import ( + LEAKAGE_TAG_MATCH, + CONDITION_TAG_MATCH, + MEASURE_TAG_MATCH, +) +from stimside.op_handlers.leakage_handlers.leakage_parameters import ( + LeakageParams, +) + +# --- CORE ARCHITECTURE --- + +@dataclass(frozen=True) +class TagDef: + """Definition and validation rules for a leakage tag.""" + name: str + parser_func: Callable[[stim.CircuitInstruction, Any, str], LeakageParams] + allowed_arity: Literal[1, 2] | None = None + allowed_gates: Sequence[str] | dict[Literal["flip", "tableau"], Sequence[str]] | None = None + supported_simulators: Sequence[Literal["flip", "tableau"]] = field( + default_factory=lambda: ("flip", "tableau") + ) + + def validate(self, op: stim.CircuitInstruction, simulator: Literal["flip", "tableau"]): + """Validate that the tag is used correctly on the given operation.""" + if simulator not in self.supported_simulators: + raise ValueError( + f"Tag '{self.name}' is not supported in the {simulator} simulator." + ) + + gd = stim.gate_data(op.name) + + if self.allowed_arity == 1 and not gd.is_single_qubit_gate: + raise ValueError(f"1Q leakage tag '{op.tag}' attached to not-1Q stim gate '{op.name}'.") + if self.allowed_arity == 2 and not gd.is_two_qubit_gate: + raise ValueError(f"2Q leakage tag '{op.tag}' attached to not-2Q stim gate '{op.name}'.") + + if self.allowed_gates is not None: + if isinstance(self.allowed_gates, dict): + allowed = self.allowed_gates.get(simulator) + else: + allowed = self.allowed_gates + + if allowed is not None and op.name not in allowed: + raise ValueError( + f"Leakage tag '{self.name}' must be attached to one of {allowed}, " + f"but was attached to '{op.name}'." + ) + + +# --- THE REGISTRY --- + +TAG_REGISTRY: dict[str, TagDef] = { + "LEAKAGE_CONTROLLED_ERROR": TagDef( + name="LEAKAGE_CONTROLLED_ERROR", + allowed_arity=2, + supported_simulators=("flip",), + allowed_gates=("I", "I_ERROR", "II", "II_ERROR"), + parser_func=ltp._parse_controlled_error + ), + "LEAKAGE_TRANSITION_1": TagDef( + name="LEAKAGE_TRANSITION_1", + allowed_arity=1, + allowed_gates={ + "flip": ("I", "I_ERROR", "II", "II_ERROR"), + "tableau": ("I", "I_ERROR", "II", "II_ERROR", "DETECTOR", "OBSERVABLE_INCLUDE") + }, + parser_func=ltp._parse_transition_1 + ), + "LEAKAGE_TRANSITION_Z": TagDef( + name="LEAKAGE_TRANSITION_Z", + allowed_arity=1, + allowed_gates=("I", "I_ERROR", "R"), + supported_simulators=("flip",), + parser_func=ltp._parse_transition_z + ), + "LEAKAGE_TRANSITION_2": TagDef( + name="LEAKAGE_TRANSITION_2", + allowed_arity=2, + allowed_gates={ + "flip": ("I", "I_ERROR", "II", "II_ERROR"), + "tableau": ("I", "I_ERROR", "II", "II_ERROR", "DETECTOR", "OBSERVABLE_INCLUDE") + }, + parser_func=ltp._parse_transition_2 + ), + "LEAKAGE_PROJECTION_Z": TagDef( + name="LEAKAGE_PROJECTION_Z", + allowed_arity=1, + allowed_gates=("M",), + parser_func=ltp._parse_projection_z + ), + "LEAKAGE_MEASUREMENT": TagDef( + name="LEAKAGE_MEASUREMENT", + allowed_gates=("MPAD",), + parser_func=ltp._parse_measurement + ), + "CONDITIONED_ON_SELF": TagDef( + name="CONDITIONED_ON_SELF", + allowed_arity=1, + supported_simulators=("tableau",), + parser_func=ltp._parse_conditioned_self + ), + "CONDITIONED_ON_OTHER": TagDef( + name="CONDITIONED_ON_OTHER", + allowed_arity=1, + supported_simulators=("tableau",), + parser_func=ltp._parse_conditioned_other + ), + "CONDITIONED_ON_PAIR": TagDef( + name="CONDITIONED_ON_PAIR", + allowed_arity=2, + supported_simulators=("tableau",), + parser_func=ltp._parse_conditioned_pair + ), + "LEAKAGE_DETECTOR": TagDef( + name="LEAKAGE_DETECTOR", + allowed_gates=("I", "I_ERROR", "II", "II_ERROR", "DETECTOR", "OBSERVABLE_INCLUDE"), + supported_simulators=("tableau",), + parser_func=lambda op, match, sim: None + ), +} + +# --- UNIFIED ENTRY POINT --- + +def parse_leakage_tag(op: stim.CircuitInstruction, simulator: Literal["flip", "tableau"]) -> LeakageParams | None: + """Parse a leakage tag using the unified registry.""" + tag = op.tag + if not (tag.startswith("LEAKAGE") or tag.startswith("CONDITIONED_ON")): + return None + + # 1. Match regex to identify name + if tag.startswith("CONDITIONED_ON"): + match = CONDITION_TAG_MATCH.fullmatch(tag) + elif tag.startswith("LEAKAGE_MEASUREMENT"): + match = MEASURE_TAG_MATCH.fullmatch(tag) + else: + match = LEAKAGE_TAG_MATCH.fullmatch(tag) + + if not match: + raise ValueError(f"Malformed leakage tag structure: {tag}") + + # 2. Lookup definition + name = match.group("name") + tag_def = TAG_REGISTRY.get(name) + if not tag_def: + raise ValueError(f"Unrecognised leakage tag name: {name}") + + # 3. Validate & Parse + tag_def.validate(op, simulator) + return tag_def.parser_func(op, match, simulator) + + +# --- CIRCUIT PARSING --- + +def _parse_leakage_in_circuit_recurse( + circuit: stim.Circuit, simulator: Literal["flip", "tableau"] +) -> dict[stim.CircuitInstruction, LeakageParams]: + """Recursive helper to parse all present leakage tags in a circuit.""" + parsed_tags = {} + for op in circuit: + if isinstance(op, stim.CircuitRepeatBlock): + parsed_tags.update( + _parse_leakage_in_circuit_recurse(op.body_copy(), simulator=simulator) + ) + elif op.tag != "": + parsed = parse_leakage_tag(op, simulator=simulator) + if parsed is not None: + parsed_tags[op] = parsed + + return parsed_tags \ No newline at end of file