// Copyright 2021 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//      http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

#include "error_analyzer.h"

#include <algorithm>
#include <iomanip>
#include <queue>
#include <sstream>

#include "../stabilizers/pauli_string.h"

using namespace stim_internal;

void ErrorAnalyzer::remove_gauge(ConstPointerRange<DemTarget> sorted) {
    if (sorted.empty()) {
        return;
    }
    const auto &max = sorted.back();
    // HACK: linear overhead due to not keeping an index of which detectors used where.
    for (auto &x : xs) {
        if (x.contains(max)) {
            x.xor_sorted_items(sorted);
        }
    }
    for (auto &z : zs) {
        if (z.contains(max)) {
            z.xor_sorted_items(sorted);
        }
    }
}

void ErrorAnalyzer::RX(const OperationData &dat) {
    for (size_t k = dat.targets.size(); k-- > 0;) {
        auto q = dat.targets[k].qubit_value();
        check_for_gauge(zs[q], "an X-basis reset");
        xs[q].clear();
        zs[q].clear();
    }
}

void ErrorAnalyzer::RY(const OperationData &dat) {
    for (size_t k = dat.targets.size(); k-- > 0;) {
        auto q = dat.targets[k].qubit_value();
        check_for_gauge(xs[q], zs[q], "a Y-basis reset");
        xs[q].clear();
        zs[q].clear();
    }
}

void ErrorAnalyzer::RZ(const OperationData &dat) {
    for (size_t k = dat.targets.size(); k-- > 0;) {
        auto q = dat.targets[k].qubit_value();
        check_for_gauge(xs[q], "a Z-basis reset");
        xs[q].clear();
        zs[q].clear();
    }
}

void ErrorAnalyzer::check_for_gauge(
    SparseXorVec<DemTarget> &potential_gauge_summand_1,
    SparseXorVec<DemTarget> &potential_gauge_summand_2,
    const char *context) {
    if (potential_gauge_summand_1 == potential_gauge_summand_2) {
        return;
    }
    potential_gauge_summand_1 ^= potential_gauge_summand_2;
    check_for_gauge(potential_gauge_summand_1, context);
    potential_gauge_summand_1 ^= potential_gauge_summand_2;
}

// This is redundant with comma_sep from str_util.h, but for reasons I can't figure out
// (something to do with a dependency cycle involving templates) the compilation fails
// if I use that one.
template <typename TIter>
std::string comma_sep_workaround(const TIter &iterable) {
    std::stringstream out;
    bool first = true;
    for (const auto &t : iterable) {
        if (first) {
            first = false;
        } else {
            out << ", ";
        }
        out << t;
    }
    return out.str();
}

void ErrorAnalyzer::check_for_gauge(const SparseXorVec<DemTarget> &potential_gauge, const char *context) {
    if (potential_gauge.empty()) {
        return;
    }
    for (const auto &t : potential_gauge) {
        if (t.is_observable_id()) {
            throw std::invalid_argument(
                "The observable " + t.str() + " anti-commuted with " + std::string(context) +
                ".\nAll objects anti-commuting with that operation: " + comma_sep_workaround(potential_gauge));
        }
    }
    if (!allow_gauge_detectors) {
        throw std::invalid_argument(
            "The detectors " + comma_sep_workaround(potential_gauge) + " anti-commuted with " + std::string(context) +
            ", and allow_gauge_detectors isn't set.");
    }
    remove_gauge(add_error(0.5, potential_gauge.range()));
}

void ErrorAnalyzer::xor_sort_measurement_error(std::vector<DemTarget> &d, const OperationData &dat) {
    std::sort(d.begin(), d.end());

    // Cancel duplicate pairs.
    size_t skip = 0;
    for (size_t k2 = 0; k2 < d.size(); k2++) {
        if (k2 + 1 < d.size() && d[k2] == d[k2 + 1]) {
            skip += 2;
            k2 += 1;
        } else if (skip) {
            d[k2 - skip] = d[k2];
        }
    }
    d.resize(d.size() - skip);

    // Measurement error.
    if (!dat.args.empty()) {
        add_error(dat.args[0], d);
    }
}

void ErrorAnalyzer::MX(const OperationData &dat) {
    for (size_t k = dat.targets.size(); k-- > 0;) {
        auto q = dat.targets[k].qubit_value();
        scheduled_measurement_time++;

        std::vector<DemTarget> &d = measurement_to_detectors[scheduled_measurement_time];
        xor_sort_measurement_error(d, dat);
        xs[q].xor_sorted_items(d);
        check_for_gauge(zs[q], "an X-basis measurement");
    }
}

void ErrorAnalyzer::MY(const OperationData &dat) {
    for (size_t k = dat.targets.size(); k-- > 0;) {
        auto q = dat.targets[k].qubit_value();
        scheduled_measurement_time++;

        std::vector<DemTarget> &d = measurement_to_detectors[scheduled_measurement_time];
        xor_sort_measurement_error(d, dat);
        xs[q].xor_sorted_items(d);
        zs[q].xor_sorted_items(d);
        check_for_gauge(xs[q], zs[q], "a Y-basis measurement");
    }
}

void ErrorAnalyzer::MZ(const OperationData &dat) {
    for (size_t k = dat.targets.size(); k-- > 0;) {
        auto q = dat.targets[k].qubit_value();
        scheduled_measurement_time++;

        std::vector<DemTarget> &d = measurement_to_detectors[scheduled_measurement_time];
        xor_sort_measurement_error(d, dat);

        zs[q].xor_sorted_items(d);
        check_for_gauge(xs[q], "a Z-basis measurement");
    }
}

void ErrorAnalyzer::MRX(const OperationData &dat) {
    for (size_t k = dat.targets.size(); k-- > 0;) {
        auto q = dat.targets[k];
        OperationData d{dat.args, {&q}};
        RX(d);
        MX(d);
    }
}

void ErrorAnalyzer::MRY(const OperationData &dat) {
    for (size_t k = dat.targets.size(); k-- > 0;) {
        auto q = dat.targets[k];
        OperationData d{dat.args, {&q}};
        RY(d);
        MY(d);
    }
}

void ErrorAnalyzer::MRZ(const OperationData &dat) {
    for (size_t k = dat.targets.size(); k-- > 0;) {
        auto q = dat.targets[k];
        OperationData d{dat.args, {&q}};
        RZ(d);
        MZ(d);
    }
}

void ErrorAnalyzer::H_XZ(const OperationData &dat) {
    for (size_t k = dat.targets.size(); k-- > 0;) {
        auto q = dat.targets[k].data;
        std::swap(xs[q], zs[q]);
    }
}

void ErrorAnalyzer::H_XY(const OperationData &dat) {
    for (size_t k = dat.targets.size(); k-- > 0;) {
        auto q = dat.targets[k].data;
        zs[q] ^= xs[q];
    }
}

void ErrorAnalyzer::H_YZ(const OperationData &dat) {
    for (size_t k = dat.targets.size(); k-- > 0;) {
        auto q = dat.targets[k].data;
        xs[q] ^= zs[q];
    }
}

void ErrorAnalyzer::C_XYZ(const OperationData &dat) {
    for (size_t k = dat.targets.size(); k-- > 0;) {
        auto q = dat.targets[k].data;
        zs[q] ^= xs[q];
        xs[q] ^= zs[q];
    }
}

void ErrorAnalyzer::C_ZYX(const OperationData &dat) {
    for (size_t k = dat.targets.size(); k-- > 0;) {
        auto q = dat.targets[k].data;
        xs[q] ^= zs[q];
        zs[q] ^= xs[q];
    }
}

void ErrorAnalyzer::XCX(const OperationData &dat) {
    for (size_t k = dat.targets.size() - 2; k + 2 != 0; k -= 2) {
        auto q1 = dat.targets[k].data;
        auto q2 = dat.targets[k + 1].data;
        xs[q1] ^= zs[q2];
        xs[q2] ^= zs[q1];
    }
}

void ErrorAnalyzer::XCY(const OperationData &dat) {
    for (size_t k = dat.targets.size() - 2; k + 2 != 0; k -= 2) {
        auto tx = dat.targets[k].data;
        auto ty = dat.targets[k + 1].data;
        xs[tx] ^= xs[ty];
        xs[tx] ^= zs[ty];
        xs[ty] ^= zs[tx];
        zs[ty] ^= zs[tx];
    }
}

void ErrorAnalyzer::YCX(const OperationData &dat) {
    for (size_t k = dat.targets.size() - 2; k + 2 != 0; k -= 2) {
        auto tx = dat.targets[k + 1].data;
        auto ty = dat.targets[k].data;
        xs[tx] ^= xs[ty];
        xs[tx] ^= zs[ty];
        xs[ty] ^= zs[tx];
        zs[ty] ^= zs[tx];
    }
}

void ErrorAnalyzer::ZCY(const OperationData &dat) {
    for (size_t k = dat.targets.size() - 2; k + 2 != 0; k -= 2) {
        auto c = dat.targets[k].data;
        auto t = dat.targets[k + 1].data;
        single_cy(c, t);
    }
}

void ErrorAnalyzer::YCZ(const OperationData &dat) {
    for (size_t k = dat.targets.size() - 2; k + 2 != 0; k -= 2) {
        auto t = dat.targets[k].data;
        auto c = dat.targets[k + 1].data;
        single_cy(c, t);
    }
}

void ErrorAnalyzer::YCY(const OperationData &dat) {
    for (size_t k = dat.targets.size() - 2; k + 2 != 0; k -= 2) {
        auto a = dat.targets[k].data;
        auto b = dat.targets[k + 1].data;
        zs[a] ^= xs[b];
        zs[a] ^= zs[b];
        xs[a] ^= xs[b];
        xs[a] ^= zs[b];

        zs[b] ^= xs[a];
        zs[b] ^= zs[a];
        xs[b] ^= xs[a];
        xs[b] ^= zs[a];
    }
}

void ErrorAnalyzer::ZCX(const OperationData &dat) {
    for (size_t k = dat.targets.size() - 2; k + 2 != 0; k -= 2) {
        auto c = dat.targets[k].data;
        auto t = dat.targets[k + 1].data;
        single_cx(c, t);
    }
}

void ErrorAnalyzer::SQRT_XX(const OperationData &dat) {
    for (size_t k = dat.targets.size() - 2; k + 2 != 0; k -= 2) {
        auto a = dat.targets[k].data;
        auto b = dat.targets[k + 1].data;
        xs[a] ^= zs[a];
        xs[a] ^= zs[b];
        xs[b] ^= zs[a];
        xs[b] ^= zs[b];
    }
}

void ErrorAnalyzer::SQRT_YY(const OperationData &dat) {
    for (size_t k = dat.targets.size() - 2; k + 2 != 0; k -= 2) {
        auto a = dat.targets[k].data;
        auto b = dat.targets[k + 1].data;
        zs[a] ^= xs[a];
        zs[b] ^= xs[b];
        xs[a] ^= zs[a];
        xs[a] ^= zs[b];
        xs[b] ^= zs[a];
        xs[b] ^= zs[b];
        zs[a] ^= xs[a];
        zs[b] ^= xs[b];
    }
}

void ErrorAnalyzer::SQRT_ZZ(const OperationData &dat) {
    for (size_t k = dat.targets.size() - 2; k + 2 != 0; k -= 2) {
        auto a = dat.targets[k].data;
        auto b = dat.targets[k + 1].data;
        zs[a] ^= xs[a];
        zs[a] ^= xs[b];
        zs[b] ^= xs[a];
        zs[b] ^= xs[b];
    }
}

void ErrorAnalyzer::feedback(uint32_t record_control, size_t target, bool x, bool z) {
    uint64_t time = scheduled_measurement_time + (record_control & ~TARGET_RECORD_BIT);
    std::vector<DemTarget> &dst = measurement_to_detectors[time];

    // Temporarily move map's vector data into a SparseXorVec for manipulation.
    std::sort(dst.begin(), dst.end());
    SparseXorVec<DemTarget> tmp(std::move(dst));

    if (x) {
        tmp ^= xs[target];
    }
    if (z) {
        tmp ^= zs[target];
    }

    // Move data back into the map.
    dst = std::move(tmp.sorted_items);
}

void ErrorAnalyzer::single_cx(uint32_t c, uint32_t t) {
    if (!((c | t) & TARGET_RECORD_BIT)) {
        zs[c] ^= zs[t];
        xs[t] ^= xs[c];
    } else if (t & TARGET_RECORD_BIT) {
        throw std::invalid_argument("Measurement record editing is not supported.");
    } else {
        feedback(c, t, false, true);
    }
}

void ErrorAnalyzer::single_cy(uint32_t c, uint32_t t) {
    if (!((c | t) & TARGET_RECORD_BIT)) {
        zs[c] ^= zs[t];
        zs[c] ^= xs[t];
        xs[t] ^= xs[c];
        zs[t] ^= xs[c];
    } else if (t & TARGET_RECORD_BIT) {
        throw std::invalid_argument("Measurement record editing is not supported.");
    } else {
        feedback(c, t, true, true);
    }
}

void ErrorAnalyzer::single_cz(uint32_t c, uint32_t t) {
    if (!((c | t) & TARGET_RECORD_BIT)) {
        zs[c] ^= xs[t];
        zs[t] ^= xs[c];
    } else if (c & TARGET_RECORD_BIT) {
        feedback(c, t, true, false);
    } else if (t & TARGET_RECORD_BIT) {
        feedback(t, c, true, false);
    } else {
        // No effect.
    }
}

void ErrorAnalyzer::XCZ(const OperationData &dat) {
    for (size_t k = dat.targets.size() - 2; k + 2 != 0; k -= 2) {
        auto t = dat.targets[k].data;
        auto c = dat.targets[k + 1].data;
        single_cx(c, t);
    }
}

void ErrorAnalyzer::ZCZ(const OperationData &dat) {
    for (size_t k = dat.targets.size() - 2; k + 2 != 0; k -= 2) {
        auto q1 = dat.targets[k].data;
        auto q2 = dat.targets[k + 1].data;
        single_cz(q1, q2);
    }
}

void ErrorAnalyzer::I(const OperationData &dat) {
}

void ErrorAnalyzer::SWAP(const OperationData &dat) {
    for (size_t k = dat.targets.size() - 2; k + 2 != 0; k -= 2) {
        auto a = dat.targets[k].data;
        auto b = dat.targets[k + 1].data;
        std::swap(xs[a], xs[b]);
        std::swap(zs[a], zs[b]);
    }
}

void ErrorAnalyzer::ISWAP(const OperationData &dat) {
    for (size_t k = dat.targets.size() - 2; k + 2 != 0; k -= 2) {
        auto a = dat.targets[k].data;
        auto b = dat.targets[k + 1].data;
        zs[a] ^= xs[a];
        zs[a] ^= xs[b];
        zs[b] ^= xs[a];
        zs[b] ^= xs[b];
        std::swap(xs[a], xs[b]);
        std::swap(zs[a], zs[b]);
    }
}

void ErrorAnalyzer::DETECTOR(const OperationData &dat) {
    used_detectors++;
    auto id = DemTarget::relative_detector_id(total_detectors - used_detectors);
    for (auto t : dat.targets) {
        auto delay = t.qubit_value();
        measurement_to_detectors[scheduled_measurement_time + delay].push_back(id);
    }
    flushed_reversed_model.append_detector_instruction(dat.args, id);
}

void ErrorAnalyzer::OBSERVABLE_INCLUDE(const OperationData &dat) {
    auto id = DemTarget::observable_id((int32_t)dat.args[0]);
    for (auto t : dat.targets) {
        auto delay = t.qubit_value();
        measurement_to_detectors[scheduled_measurement_time + delay].push_back(id);
    }
    flushed_reversed_model.append_logical_observable_instruction(id);
}

ErrorAnalyzer::ErrorAnalyzer(
    uint64_t num_detectors,
    size_t num_qubits,
    bool decompose_errors,
    bool fold_loops,
    bool allow_gauge_detectors,
    double approximate_disjoint_errors_threshold)
    : total_detectors(num_detectors),
      used_detectors(0),
      xs(num_qubits),
      zs(num_qubits),
      scheduled_measurement_time(0),
      decompose_errors(decompose_errors),
      accumulate_errors(true),
      fold_loops(fold_loops),
      allow_gauge_detectors(allow_gauge_detectors),
      approximate_disjoint_errors_threshold(approximate_disjoint_errors_threshold) {
}

void ErrorAnalyzer::run_circuit(const Circuit &circuit) {
    for (size_t k = circuit.operations.size(); k--;) {
        const auto &op = circuit.operations[k];
        assert(op.gate != nullptr);
        if (op.gate->id == gate_name_to_id("REPEAT")) {
            assert(op.target_data.targets.size() == 3);
            auto b = op.target_data.targets[0].data;
            assert(op.target_data.targets[0].data < circuit.blocks.size());
            uint64_t repeats = op_data_rep_count(op.target_data);
            const auto &block = circuit.blocks[b];
            try {
                run_loop(block, repeats);
            } catch (std::invalid_argument &ex) {
                throw std::invalid_argument(
                    std::string(ex.what()) + "\nContext: inside of the REPEAT block at offset " + std::to_string(k) +
                    ".");
            }
        } else {
            try {
                (this->*op.gate->reverse_error_analyzer_function)(op.target_data);
            } catch (std::invalid_argument &ex) {
                throw std::invalid_argument(
                    std::string(ex.what()) + "\nContext: analyzing the circuit operation at offset " +
                    std::to_string(k) + " which is '" + op.str() + "'.");
            }
        }
    }
}

void ErrorAnalyzer::post_check_initialization() {
    for (const auto &x : xs) {
        check_for_gauge(x, "qubit initialization into |0> at the start of the circuit");
    }
}

void ErrorAnalyzer::X_ERROR(const OperationData &dat) {
    if (!accumulate_errors) {
        return;
    }
    for (auto q : dat.targets) {
        add_error(dat.args[0], zs[q.data].range());
    }
}

void ErrorAnalyzer::Y_ERROR(const OperationData &dat) {
    if (!accumulate_errors) {
        return;
    }
    for (auto q : dat.targets) {
        add_xored_error(dat.args[0], xs[q.data].range(), zs[q.data].range());
    }
}

void ErrorAnalyzer::Z_ERROR(const OperationData &dat) {
    if (!accumulate_errors) {
        return;
    }
    for (auto q : dat.targets) {
        add_error(dat.args[0], xs[q.data].range());
    }
}

template <typename T>
inline void inplace_xor_tail(MonotonicBuffer<T> &dst, const SparseXorVec<T> &src) {
    ConstPointerRange<T> in1 = dst.tail;
    ConstPointerRange<T> in2 = src.range();
    xor_merge_sort_temp_buffer_callback(in1, in2, [&](ConstPointerRange<T> result) {
        dst.discard_tail();
        dst.append_tail(result);
    });
}

void ErrorAnalyzer::CORRELATED_ERROR(const OperationData &dat) {
    if (!accumulate_errors) {
        return;
    }
    for (auto qp : dat.targets) {
        auto q = qp.qubit_value();
        if (qp.data & TARGET_PAULI_Z_BIT) {
            inplace_xor_tail(mono_buf, xs[q]);
        }
        if (qp.data & TARGET_PAULI_X_BIT) {
            inplace_xor_tail(mono_buf, zs[q]);
        }
    }
    add_error_in_sorted_jagged_tail(dat.args[0]);
}

void ErrorAnalyzer::DEPOLARIZE1(const OperationData &dat) {
    if (!accumulate_errors) {
        return;
    }
    if (dat.args[0] >= 3.0 / 4.0) {
        throw std::invalid_argument("Can't analyze over-mixing DEPOLARIZE1 errors (probability >= 3/4).");
    }
    double p = 0.5 - 0.5 * sqrt(1 - (4 * dat.args[0]) / 3);
    for (auto q : dat.targets) {
        add_error_combinations<2>(
            {0, p, p, p},
            {
                xs[q.data].range(),
                zs[q.data].range(),
            });
    }
}

void ErrorAnalyzer::DEPOLARIZE2(const OperationData &dat) {
    if (!accumulate_errors) {
        return;
    }
    if (dat.args[0] >= 15.0 / 16.0) {
        throw std::invalid_argument("Can't analyze over-mixing DEPOLARIZE2 errors (probability >= 15/16).");
    }
    double p = 0.5 - 0.5 * pow(1 - (16 * dat.args[0]) / 15, 0.125);
    for (size_t i = 0; i < dat.targets.size(); i += 2) {
        auto a = dat.targets[i];
        auto b = dat.targets[i + 1];
        add_error_combinations<4>(
            {0, p, p, p, p, p, p, p, p, p, p, p, p, p, p, p},
            {
                xs[a.data].range(),
                zs[a.data].range(),
                xs[b.data].range(),
                zs[b.data].range(),
            });
    }
}

void ErrorAnalyzer::ELSE_CORRELATED_ERROR(const OperationData &dat) {
    throw std::invalid_argument(
        "ELSE_CORRELATED_ERROR operations currently not supported in error analysis (cases may not be independent).");
}

void ErrorAnalyzer::PAULI_CHANNEL_1(const OperationData &dat) {
    if (approximate_disjoint_errors_threshold == 0) {
        throw std::invalid_argument(
            "Handling PAULI_CHANNEL_1 requires `approximate_disjoint_errors` argument to be specified.");
    }
    PointerRange<double> args = dat.args;
    std::array<double, 4> probabilities;
    for (size_t k = 0; k < 3; k++) {
        if (args[k] > approximate_disjoint_errors_threshold) {
            throw std::invalid_argument(
                "PAULI_CHANNEL_1 has a component probability '" + std::to_string(args[k]) +
                "' larger than the "
                "`approximate_disjoint_errors` threshold of "
                "'" +
                std::to_string(approximate_disjoint_errors_threshold) + "'.");
        }
        probabilities[pauli_xyz_to_xz(k + 1)] = args[k];
    }
    if (!accumulate_errors) {
        return;
    }
    for (auto q : dat.targets) {
        add_error_combinations<2>(
            probabilities,
            {
                zs[q.data].range(),
                xs[q.data].range(),
            });
    }
}

void ErrorAnalyzer::PAULI_CHANNEL_2(const OperationData &dat) {
    if (approximate_disjoint_errors_threshold == 0) {
        throw std::invalid_argument(
            "Handling PAULI_CHANNEL_2 requires `approximate_disjoint_errors` argument to be specified.");
    }
    PointerRange<double> args = dat.args;
    std::array<double, 16> probabilities;
    for (size_t k = 0; k < 15; k++) {
        if (args[k] > approximate_disjoint_errors_threshold) {
            throw std::invalid_argument(
                "PAULI_CHANNEL_2 has a component probability '" + std::to_string(args[k]) +
                "' larger than the "
                "`approximate_disjoint_errors` threshold of "
                "'" +
                std::to_string(approximate_disjoint_errors_threshold) + "'.");
        }
        size_t k2 = pauli_xyz_to_xz((k + 1) & 3) | (pauli_xyz_to_xz(((k + 1) >> 2) & 3) << 2);
        probabilities[k2] = args[k];
    }
    if (!accumulate_errors) {
        return;
    }
    for (size_t i = 0; i < dat.targets.size(); i += 2) {
        auto a = dat.targets[i];
        auto b = dat.targets[i + 1];
        add_error_combinations<4>(
            probabilities,
            {
                zs[b.data].range(),
                xs[b.data].range(),
                zs[a.data].range(),
                xs[a.data].range(),
            });
    }
}

DetectorErrorModel unreversed(const DetectorErrorModel &rev, uint64_t &base_detector_id, std::set<DemTarget> &seen) {
    DetectorErrorModel out;
    auto conv_append = [&](const DemInstruction &e) {
        auto stored_targets = out.target_buf.take_copy(e.target_data);
        auto stored_args = out.arg_buf.take_copy(e.arg_data);
        for (auto &t : stored_targets) {
            t.shift_if_detector_id(-(int64_t)base_detector_id);
        }
        out.instructions.push_back(DemInstruction{stored_args, stored_targets, e.type});
    };

    for (auto p = rev.instructions.crbegin(); p != rev.instructions.crend(); p++) {
        const auto &e = *p;
        switch (e.type) {
            case DEM_SHIFT_DETECTORS:
                base_detector_id += e.target_data[0].data;
                out.append_shift_detectors_instruction(e.arg_data, e.target_data[0].data);
                break;
            case DEM_ERROR:
                for (auto &t : e.target_data) {
                    seen.insert(t);
                }
                conv_append(e);
                break;
            case DEM_DETECTOR:
            case DEM_LOGICAL_OBSERVABLE:
                if (!e.arg_data.empty() || seen.find(e.target_data[0]) == seen.end()) {
                    conv_append(e);
                }
                break;
            case DEM_REPEAT_BLOCK: {
                uint64_t repetitions = e.target_data[0].data;
                if (repetitions) {
                    uint64_t old_base_detector_id = base_detector_id;
                    out.append_repeat_block(
                        e.target_data[0].data, unreversed(rev.blocks[e.target_data[1].data], base_detector_id, seen));
                    uint64_t loop_shift = base_detector_id - old_base_detector_id;
                    base_detector_id += loop_shift * (repetitions - 1);
                }
            } break;
            default:
                throw std::invalid_argument("Unknown instruction type in 'unreversed'.");
        }
    }
    return out;
}

DetectorErrorModel ErrorAnalyzer::circuit_to_detector_error_model(
    const Circuit &circuit,
    bool decompose_errors,
    bool fold_loops,
    bool allow_gauge_detectors,
    double approximate_disjoint_errors_threshold) {
    ErrorAnalyzer analyzer(
        circuit.count_detectors(),
        circuit.count_qubits(),
        decompose_errors,
        fold_loops,
        allow_gauge_detectors,
        approximate_disjoint_errors_threshold);
    analyzer.run_circuit(circuit);
    analyzer.post_check_initialization();
    analyzer.flush();
    uint64_t t = 0;
    std::set<DemTarget> seen;
    return unreversed(analyzer.flushed_reversed_model, t, seen);
}

void ErrorAnalyzer::flush() {
    do_global_error_decomposition_pass();
    for (auto kv = error_class_probabilities.crbegin(); kv != error_class_probabilities.crend(); kv++) {
        if (kv->first.empty() || kv->second == 0) {
            continue;
        }
        flushed_reversed_model.append_error_instruction(kv->second, kv->first);
    }
    error_class_probabilities.clear();
}

ConstPointerRange<DemTarget> ErrorAnalyzer::add_xored_error(
    double probability, ConstPointerRange<DemTarget> flipped1, ConstPointerRange<DemTarget> flipped2) {
    mono_buf.ensure_available(flipped1.size() + flipped2.size());
    mono_buf.tail.ptr_end = xor_merge_sort(flipped1, flipped2, mono_buf.tail.ptr_end);
    return add_error_in_sorted_jagged_tail(probability);
}

ConstPointerRange<DemTarget> ErrorAnalyzer::mono_dedupe_store_tail() {
    auto v = error_class_probabilities.find(mono_buf.tail);
    if (v != error_class_probabilities.end()) {
        mono_buf.discard_tail();
        return v->first;
    }
    auto result = mono_buf.commit_tail();
    error_class_probabilities.insert({result, 0});
    return result;
}

ConstPointerRange<DemTarget> ErrorAnalyzer::mono_dedupe_store(ConstPointerRange<DemTarget> sorted) {
    auto v = error_class_probabilities.find(sorted);
    if (v != error_class_probabilities.end()) {
        return v->first;
    }
    mono_buf.append_tail(sorted);
    auto result = mono_buf.commit_tail();
    error_class_probabilities.insert({result, 0});
    return result;
}

ConstPointerRange<DemTarget> ErrorAnalyzer::add_error(double probability, ConstPointerRange<DemTarget> flipped_sorted) {
    auto key = mono_dedupe_store(flipped_sorted);
    auto &old_p = error_class_probabilities[key];
    old_p = old_p * (1 - probability) + (1 - old_p) * probability;
    return key;
}

ConstPointerRange<DemTarget> ErrorAnalyzer::add_error_in_sorted_jagged_tail(double probability) {
    auto key = mono_dedupe_store_tail();
    auto &old_p = error_class_probabilities[key];
    old_p = old_p * (1 - probability) + (1 - old_p) * probability;
    return key;
}

bool shifted_equals(int64_t shift, const SparseXorVec<DemTarget> &unshifted, const SparseXorVec<DemTarget> &expected) {
    if (unshifted.size() != expected.size()) {
        return false;
    }
    for (size_t k = 0; k < unshifted.size(); k++) {
        DemTarget a = unshifted.sorted_items[k];
        DemTarget e = expected.sorted_items[k];
        a.shift_if_detector_id(shift);
        if (a != e) {
            return false;
        }
    }
    return true;
}

void ErrorAnalyzer::run_loop(const Circuit &loop, uint64_t iterations) {
    if (!fold_loops) {
        // If loop folding is disabled, just manually run each iteration.
        for (size_t k = 0; k < iterations; k++) {
            run_circuit(loop);
        }
        return;
    }

    uint64_t num_loop_detectors = loop.count_detectors();
    uint64_t hare_iter = 0;
    uint64_t tortoise_iter = 0;
    ErrorAnalyzer hare(
        total_detectors - used_detectors,
        xs.size(),
        false,
        true,
        allow_gauge_detectors,
        approximate_disjoint_errors_threshold);
    hare.xs = xs;
    hare.zs = zs;
    hare.measurement_to_detectors = measurement_to_detectors;
    hare.scheduled_measurement_time = scheduled_measurement_time;
    hare.accumulate_errors = false;

    auto hare_is_colliding_with_tortoise = [&]() -> bool {
        // When comparing different loop iterations, shift detector ids to account for
        // detectors being introduced during each iteration.
        int64_t dt = -(int64_t)((hare_iter - tortoise_iter) * num_loop_detectors);
        for (size_t k = 0; k < hare.xs.size(); k++) {
            if (!shifted_equals(dt, xs[k], hare.xs[k])) {
                return false;
            }
            if (!shifted_equals(dt, zs[k], hare.zs[k])) {
                return false;
            }
        }
        return true;
    };

    // Perform tortoise-and-hare cycle finding.
    while (hare_iter < iterations) {
        hare.run_circuit(loop);
        hare_iter++;
        if (hare_is_colliding_with_tortoise()) {
            break;
        }

        if (hare_iter % 2 == 0) {
            run_circuit(loop);
            tortoise_iter++;
            if (hare_is_colliding_with_tortoise()) {
                break;
            }
        }
    }

    if (hare_iter < iterations) {
        // Don't bother folding a single iteration into a repeated block.
        uint64_t period = hare_iter - tortoise_iter;
        uint64_t period_iterations = (iterations - tortoise_iter) / period;
        if (period_iterations > 1) {
            // Stash error model build up so far.
            flush();
            DetectorErrorModel tmp = std::move(flushed_reversed_model);

            // Rewrite state to look like it would if loop had executed all but the last iteration.
            uint64_t shift_per_iteration = period * num_loop_detectors;
            int64_t detector_shift = (int64_t)((period_iterations - 1) * shift_per_iteration);
            shift_active_detector_ids(-detector_shift);
            used_detectors += detector_shift;
            tortoise_iter += period_iterations * period;

            // Compute the loop's error model.
            for (size_t k = 0; k < period; k++) {
                run_circuit(loop);
            }
            flush();
            DetectorErrorModel body = std::move(flushed_reversed_model);

            // The loop ends (well, starts because everything is reversed) by shifting the detector coordinates.
            uint64_t lower_level_shifts = body.total_detector_shift();
            DemTarget remaining_shift = {shift_per_iteration - lower_level_shifts};
            if (remaining_shift.data > 0) {
                if (body.instructions.empty() || body.instructions.front().type != DEM_SHIFT_DETECTORS) {
                    auto shift_targets = body.target_buf.take_copy({&remaining_shift});
                    body.instructions.insert(
                        body.instructions.begin(), DemInstruction{{}, shift_targets, DEM_SHIFT_DETECTORS});
                } else {
                    remaining_shift.data += body.instructions[0].target_data[0].data;
                    auto shift_targets = body.target_buf.take_copy({&remaining_shift});
                    body.instructions[0].target_data = shift_targets;
                }
            }

            // Append the loop to the growing error model and put the error model back in its proper place.
            tmp.append_repeat_block(period_iterations, std::move(body));
            flushed_reversed_model = std::move(tmp);
        }
    }

    // Perform remaining loop iterations leftover after jumping forward by multiples of the recurrence period.
    while (tortoise_iter < iterations) {
        run_circuit(loop);
        tortoise_iter++;
    }
}

void ErrorAnalyzer::shift_active_detector_ids(int64_t shift) {
    for (auto &e : measurement_to_detectors) {
        for (auto &v : e.second) {
            v.shift_if_detector_id(shift);
        }
    }
    for (auto &x : xs) {
        for (auto &v : x) {
            v.shift_if_detector_id(shift);
        }
    }
    for (auto &x : zs) {
        for (auto &v : x) {
            v.shift_if_detector_id(shift);
        }
    }
}

void ErrorAnalyzer::SHIFT_COORDS(const OperationData &dat) {
    flushed_reversed_model.append_shift_detectors_instruction(dat.args, 0);
}

template <size_t s>
void ErrorAnalyzer::decompose_helper_add_error_combinations(
    const std::array<uint64_t, 1 << s> &detector_masks, std::array<ConstPointerRange<DemTarget>, 1 << s> &stored_ids) {
    // Count number of detectors affected by each error.
    std::array<uint8_t, 1 << s> detector_counts{};
    for (size_t k = 1; k < 1 << s; k++) {
        detector_counts[k] = popcnt64(detector_masks[k]);
    }

    // Find single-detector errors (and empty errors).
    uint64_t solved = 0;
    uint64_t single_detectors_union = 0;
    for (size_t k = 1; k < 1 << s; k++) {
        if (detector_counts[k] == 1) {
            single_detectors_union |= detector_masks[k];
            solved |= 1 << k;
        }
    }

    // Find irreducible double-detector errors.
    FixedCapVector<uint8_t, 1 << s> irreducible_pairs{};
    for (size_t k = 1; k < 1 << s; k++) {
        if (detector_counts[k] == 2 && (detector_masks[k] & ~single_detectors_union)) {
            irreducible_pairs.push_back(k);
            solved |= 1 << k;
        }
    }

    auto append_involved_pairs_to_jag_tail = [&](size_t goal_k) -> uint64_t {
        uint64_t goal = detector_masks[goal_k];

        // If single-detector excitations are sufficient, just use those.
        if ((goal & ~single_detectors_union) == 0) {
            return goal;
        }

        // Check if one double-detector excitation can get us into the single-detector region.
        for (auto k : irreducible_pairs) {
            auto m = detector_masks[k];
            if ((goal & m) == m && (goal & ~(single_detectors_union | m)) == 0) {
                mono_buf.append_tail(stored_ids[k]);
                mono_buf.append_tail(DemTarget::separator());
                return goal & ~m;
            }
        }

        // Check if two double-detector excitations can get us into the single-detector region.
        for (size_t i1 = 0; i1 < irreducible_pairs.size(); i1++) {
            auto k1 = irreducible_pairs[i1];
            auto m1 = detector_masks[k1];
            for (size_t i2 = i1 + 1; i2 < irreducible_pairs.size(); i2++) {
                auto k2 = irreducible_pairs[i2];
                auto m2 = detector_masks[k2];
                if ((m1 & m2) == 0 && (goal & ~(single_detectors_union | m1 | m2)) == 0) {
                    if (stored_ids[k2] < stored_ids[k1]) {
                        std::swap(k1, k2);
                    }
                    mono_buf.append_tail(stored_ids[k1]);
                    mono_buf.append_tail(DemTarget::separator());
                    mono_buf.append_tail(stored_ids[k2]);
                    mono_buf.append_tail(DemTarget::separator());
                    return goal & ~(m1 | m2);
                }
            }
        }

        // Failed to decompose into other components of the same composite Pauli channel.
        // Put it into the result undecomposed, to be worked on more later.
        mono_buf.append_tail(stored_ids[goal_k]);
        mono_buf.append_tail(DemTarget::separator());
        return 0;
    };

    // Solve the decomposition of each composite case.
    for (size_t k = 1; k < 1 << s; k++) {
        if (detector_counts[k] && ((solved >> k) & 1) == 0) {
            auto remnants = append_involved_pairs_to_jag_tail(k);

            // Finish off the solution using single-detector components.
            for (size_t k2 = 0; remnants && k2 < 1 << s; k2++) {
                if (detector_counts[k2] == 1 && (detector_masks[k2] & ~remnants) == 0) {
                    remnants &= ~detector_masks[k2];
                    mono_buf.append_tail(stored_ids[k2]);
                    mono_buf.append_tail(DemTarget::separator());
                }
            }
            if (!mono_buf.tail.empty()) {
                mono_buf.tail.ptr_end -= 1;
            }
            stored_ids[k] = mono_dedupe_store_tail();
        }
    }
}

bool stim_internal::is_graphlike(const ConstPointerRange<DemTarget> &components) {
    size_t symptom_count = 0;
    for (const auto &t : components) {
        if (t.is_separator()) {
            symptom_count = 0;
        } else if (t.is_relative_detector_id()) {
            symptom_count++;
            if (symptom_count > 2) {
                return false;
            }
        }
    }
    return true;
}

bool ErrorAnalyzer::has_unflushed_ungraphlike_errors() const {
    for (const auto &kv : error_class_probabilities) {
        const auto &component = kv.first;
        if (kv.second != 0 && !is_graphlike(component)) {
            return true;
        }
    }
    return false;
}

void ErrorAnalyzer::decompose_and_append_component_to_tail(
    ConstPointerRange<DemTarget> component,
    const std::map<FixedCapVector<DemTarget, 2>, ConstPointerRange<DemTarget>> &known_symptoms) {
    std::vector<bool> done(component.size(), false);

    size_t num_component_detectors = 0;
    for (size_t k = 0; k < component.size(); k++) {
        if (component[k].is_relative_detector_id()) {
            num_component_detectors++;
        } else {
            done[k] = true;
        }
    }
    if (num_component_detectors <= 2) {
        mono_buf.append_tail(component);
        mono_buf.append_tail(DemTarget::separator());
        return;
    }

    SparseXorVec<DemTarget> sparse;
    sparse.xor_sorted_items(component);

    for (size_t k = 0; k < component.size(); k++) {
        if (!done[k]) {
            for (size_t k2 = k + 1; k2 < component.size(); k2++) {
                if (!done[k2]) {
                    auto p = known_symptoms.find({component[k], component[k2]});
                    if (p != known_symptoms.end()) {
                        done[k] = true;
                        done[k2] = true;
                        mono_buf.append_tail(p->second);
                        mono_buf.append_tail(DemTarget::separator());
                        sparse.xor_sorted_items(p->second);
                        break;
                    }
                }
            }
        }
    }

    size_t missed = 0;
    for (size_t k = 0; k < component.size(); k++) {
        if (!done[k]) {
            auto p = known_symptoms.find({component[k]});
            if (p != known_symptoms.end()) {
                done[k] = true;
                mono_buf.append_tail(p->second);
                mono_buf.append_tail(DemTarget::separator());
                sparse.xor_sorted_items(p->second);
            }
        }
        missed += !done[k];
    }

    if (missed > 2) {
        throw std::invalid_argument(
            "Failed to decompose errors into graphlike components with at most two symptoms.\n"
            "The error component that failed to decompose is '" +
            comma_sep_workaround(component) + "'.");
    }

    if (!sparse.empty()) {
        mono_buf.append_tail({sparse.begin(), sparse.end()});
        mono_buf.append_tail(DemTarget::separator());
    }
}

void ErrorAnalyzer::do_global_error_decomposition_pass() {
    if (!decompose_errors || !has_unflushed_ungraphlike_errors()) {
        return;
    }

    std::vector<DemTarget> component_symptoms;

    // Make a map from all known symptoms singlets and pairs to actual components including frame changes.
    std::map<FixedCapVector<DemTarget, 2>, ConstPointerRange<DemTarget>> known_symptoms;
    for (const auto &kv : error_class_probabilities) {
        if (kv.second == 0 || kv.first.empty()) {
            continue;
        }
        const auto &targets = kv.first;
        size_t start = 0;
        for (size_t k = 0; k <= targets.size(); k++) {
            if (k == targets.size() || targets[k].is_separator()) {
                if (component_symptoms.size() == 1) {
                    known_symptoms[{component_symptoms[0]}] = {&targets[start], &targets[k]};
                } else if (component_symptoms.size() == 2) {
                    known_symptoms[{component_symptoms[0], component_symptoms[1]}] = {&targets[start], &targets[k]};
                }
                component_symptoms.clear();
                start = k + 1;
            } else if (targets[k].is_relative_detector_id()) {
                component_symptoms.push_back(targets[k]);
            }
        }
    }

    // Find how to rewrite hyper errors into graphlike errors.
    std::vector<std::pair<ConstPointerRange<DemTarget>, ConstPointerRange<DemTarget>>> rewrites;
    for (const auto &kv : error_class_probabilities) {
        if (kv.second == 0 || kv.first.empty()) {
            continue;
        }

        const auto &targets = kv.first;
        if (is_graphlike(targets)) {
            continue;
        }

        size_t start = 0;
        for (size_t k = 0; k <= targets.size(); k++) {
            if (k == targets.size() || targets[k].is_separator()) {
                decompose_and_append_component_to_tail({&targets[start], &targets[k]}, known_symptoms);
                start = k + 1;
            }
        }

        if (!mono_buf.tail.empty()) {
            // Drop final separator.
            mono_buf.tail.ptr_end -= 1;
        }

        rewrites.push_back({kv.first, mono_buf.commit_tail()});
    }

    for (const auto &rewrite : rewrites) {
        add_error(error_class_probabilities[rewrite.first], rewrite.second);
        error_class_probabilities.erase(rewrite.first);
    }
}

template <size_t s>
void ErrorAnalyzer::add_error_combinations(
    std::array<double, 1 << s> independent_probabilities, std::array<ConstPointerRange<DemTarget>, s> basis_errors) {
    std::array<uint64_t, 1 << s> detector_masks{};
    FixedCapVector<DemTarget, 16> involved_detectors{};
    std::array<ConstPointerRange<DemTarget>, 1 << s> stored_ids;

    for (size_t k = 0; k < s; k++) {
        for (const auto &id : basis_errors[k]) {
            if (id.is_relative_detector_id()) {
                auto r = involved_detectors.find(id);
                if (r == involved_detectors.end()) {
                    try {
                        involved_detectors.push_back(id);
                    } catch (const std::out_of_range &ex) {
                        std::stringstream message;
                        message << "An error case in a composite error exceeded that max supported number of symptoms "
                                   "(<=15). ";
                        message << "\nThe " << std::to_string(s)
                                << " basis error cases (e.g. X, Z) used to form the combined ";
                        message << "error cases (e.g. Y = X*Z) are:\n";
                        for (size_t k2 = 0; k2 < s; k2++) {
                            message << std::to_string(k2) << ": " << comma_sep_workaround(basis_errors[k2]) << "\n";
                        }
                        throw std::invalid_argument(message.str());
                    }
                }
                detector_masks[1 << k] ^= 1 << (r - involved_detectors.begin());
            }
        }
        stored_ids[1 << k] = mono_dedupe_store(basis_errors[k]);
    }

    // Fill in all 2**s - 1 possible combinations from the initial basis values.
    for (size_t k = 3; k < 1 << s; k++) {
        auto c1 = k & (k - 1);
        auto c2 = k ^ c1;
        if (c1) {
            mono_buf.ensure_available(stored_ids[c1].size() + stored_ids[c2].size());
            mono_buf.tail.ptr_end = xor_merge_sort(stored_ids[c1], stored_ids[c2], mono_buf.tail.ptr_end);
            stored_ids[k] = mono_dedupe_store_tail();
            detector_masks[k] = detector_masks[c1] ^ detector_masks[c2];
        }
    }

    // Determine involved detectors while creating basis masks and storing added data.
    if (decompose_errors) {
        decompose_helper_add_error_combinations<s>(detector_masks, stored_ids);
    }

    // Include errors in the record.
    for (size_t k = 1; k < 1 << s; k++) {
        add_error(independent_probabilities[k], stored_ids[k]);
    }
}

void ErrorAnalyzer::MPP(const OperationData &target_data) {
    decompose_mpp_operation(
        target_data,
        xs.size(),
        [&](const OperationData &h_xz,
            const OperationData &h_yz,
            const OperationData &cnot,
            const OperationData &meas) {
            H_XZ(h_xz);
            H_YZ(h_yz);
            ZCX(cnot);
            MZ(meas);
            ZCX(cnot);
            H_YZ(h_yz);
            H_XZ(h_xz);
        });
}
