def add_dynamical_decoupling(
    circuit: cirq.AbstractCircuit,
    *,
    context: cirq.TransformerContext | None = None,
    schema: str | tuple[ops.Gate, ...] = 'DEFAULT',
    single_qubit_gate_moments_only: bool = True,
) -> cirq.Circuit:
    """Adds dynamical decoupling gate operations to a given circuit.
    This transformer might add new moments thus change structure of the original circuit.

    Args:
          circuit: Input circuit to transform.
          context: `cirq.TransformerContext` storing common configurable options for transformers.
          schema: Dynamical decoupling schema name or a dynamical decoupling sequence.
            If a schema is specified, provided dynamical decouping sequence will be used.
            Otherwise, customized dynamical decoupling sequence will be applied.
          single_qubit_gate_moments_only: If set True, dynamical decoupling operation will only be
            added in single-qubit gate moments.

    Returns:
          A copy of the input circuit with dynamical decoupling operations.
    """
    base_dd_sequence, pauli_map = _parse_dd_sequence(schema)
    orig_circuit = circuit.freeze()

    busy_moment_range_by_qubit = _calc_busy_moment_range_of_each_qubit(orig_circuit)

    # Stores all the moments of the output circuit chronologically.
    transformed_moments: list[circuits.Moment] = []
    # A PauliString stores the result of 'pulling' Pauli gates past each operations
    # right before the current moment.
    pulled_through: ops.PauliString = ops.PauliString()
    # Iterator of gate to be used in dd sequence for each qubit.
    dd_iter_by_qubits = {q: cycle(base_dd_sequence) for q in circuit.all_qubits()}

    def _update_pulled_through(q: ops.Qid, insert_gate: ops.Gate) -> ops.Operation:
        nonlocal pulled_through, pauli_map
        pulled_through *= pauli_map[insert_gate].on(q)
        return insert_gate.on(q)

    # Insert and pull remaining Pauli ops through the whole circuit.
    # General ideas are
    #   * Pull through Clifford gates.
    #   * Stop at multi-qubit non-Clifford ops (and other non-mergable ops).
    #   * Merge to single-qubit non-Clifford ops.
    #   * Insert a new moment if necessary.
    # After pulling through pulled_through at `moment`, we expect a transformation of
    #  (pulled_through, moment) -> (updated_moment, updated_pulled_through) or
    #  (pulled_through, moment) -> (new_moment, updated_moment, updated_pulled_through)
    # Moments structure changes are split into 3 steps:
    #   1, (..., last_moment, pulled_through1, moment, ...)
    #        -> (..., try_merge(last_moment, new_moment or None), pulled_through2, moment, ...)
    #   2, (..., pulled_through2, moment, ...)  -> (..., pulled_through3, updated_moment, ...)
    #   3, (..., pulled_through3, updated_moment, ...)
    #        -> (..., updated_moment, pulled_through4, ...)
    for moment_id, moment in enumerate(orig_circuit.moments):
        # Step 1, insert new_moment if necessary.
        # In detail: stop pulling through for multi-qubit non-Clifford ops or gates without
        # unitary representation (e.g., measure gates). If there are remaining pulled through ops,
        # insert into a new moment before current moment.
        stop_pulling_through_qubits: set[ops.Qid] = _get_stop_qubits(moment)
        new_moment_ops = []
        for q in stop_pulling_through_qubits:
            # Insert the remaining pulled_through
            remaining_pulled_through_gate = pulled_through.get(q)
            if remaining_pulled_through_gate is not None:
                new_moment_ops.append(_update_pulled_through(q, remaining_pulled_through_gate))
            # Reset dd sequence
            dd_iter_by_qubits[q] = cycle(base_dd_sequence)
        # Need to insert a new moment before current moment
        if new_moment_ops:
            moments_to_be_appended = _try_merge_single_qubit_ops_of_two_moments(
                transformed_moments[-1], circuits.Moment(new_moment_ops)
            )
            if len(moments_to_be_appended) == 1:
                transformed_moments.pop()
                transformed_moments.append(moments_to_be_appended[0])
            else:  # Fill insertable idle moments in the new moment using dd sequence
                for q in orig_circuit.all_qubits() - stop_pulling_through_qubits:
                    if (
                        busy_moment_range_by_qubit[q][0]
                        < moment_id
                        <= busy_moment_range_by_qubit[q][1]
                    ):
                        new_moment_ops.append(_update_pulled_through(q, next(dd_iter_by_qubits[q])))
                moments_to_be_appended = _try_merge_single_qubit_ops_of_two_moments(
                    transformed_moments.pop(), circuits.Moment(new_moment_ops)
                )
                transformed_moments.extend(moments_to_be_appended)

        # Step 2, calc updated_moment with insertions / merges.
        updated_moment_ops: set[cirq.Operation] = set()
        for q in orig_circuit.all_qubits():
            op_at_q = moment.operation_at(q)
            remaining_pulled_through_gate = pulled_through.get(q)
            updated_op = op_at_q
            if op_at_q is None:  # insert into idle op
                if not _is_insertable_moment(moment, single_qubit_gate_moments_only):
                    continue
                if (
                    busy_moment_range_by_qubit[q][0] < moment_id < busy_moment_range_by_qubit[q][1]
                ):  # insert next pauli gate in the dd sequence
                    updated_op = _update_pulled_through(q, next(dd_iter_by_qubits[q]))
                elif (  # insert the remaining pulled through if beyond the ending busy moment
                    moment_id > busy_moment_range_by_qubit[q][1]
                    and remaining_pulled_through_gate is not None
                ):
                    updated_op = _update_pulled_through(q, remaining_pulled_through_gate)
            elif (
                remaining_pulled_through_gate is not None
            ):  # merge pulled-through of q to op_at_q if needed
                if _need_merge_pulled_through(
                    op_at_q, moment_id == busy_moment_range_by_qubit[q][1]
                ):
                    remaining_op = _update_pulled_through(q, remaining_pulled_through_gate)
                    updated_op = _merge_single_qubit_ops_to_phxz(q, (remaining_op, op_at_q))
            if updated_op is not None:
                updated_moment_ops.add(updated_op)

        if updated_moment_ops:
            updated_moment = circuits.Moment(updated_moment_ops)
            transformed_moments.append(updated_moment)

            # Step 3, update pulled through.
            # In detail: pulling current `pulled_through` through updated_moment.
            pulled_through = _calc_pulled_through(updated_moment, pulled_through)

    # Insert a new moment if there are remaining pulled-through operations.
    ending_moment_ops = []
    for affected_q, combined_op_in_pauli in pulled_through.items():
        ending_moment_ops.append(combined_op_in_pauli.on(affected_q))
    if ending_moment_ops:
        transformed_moments.extend(
            _try_merge_single_qubit_ops_of_two_moments(
                transformed_moments.pop(), circuits.Moment(ending_moment_ops)
            )
        )

    return circuits.Circuit(transformed_moments)
Adds dynamical decoupling gate operations to a given circuit.
    This transformer might add new moments thus change structure of the original circuit.

    Args:
          circuit: Input circuit to transform.
          context: `cirq.TransformerContext` storing common configurable options for transformers.
          schema: Dynamical decoupling schema name or a dynamical decoupling sequence.
            If a schema is specified, provided dynamical decouping sequence will be used.
            Otherwise, customized dynamical decoupling sequence will be applied.
          single_qubit_gate_moments_only: If set True, dynamical decoupling operation will only be
            added in single-qubit gate moments.

    Returns:
          A copy of the input circuit with dynamical decoupling operations.
    
cirq-core/cirq/transformers/dynamical_decoupling.py