{
  "cells": [
    {
      "cell_type": "markdown",
      "id": "454a9dfd",
      "metadata": {},
      "source": [
        "---\n",
        "title: \"트로터 오류를 줄이기 위한 다중 제품 공식\"\n",
        "description: \"관측 가능 추정에서 다중 제품 공식을 사용하여 트로터 오차를 줄이거나, 더 낮은 깊이에서 트로터 오차를 고정시킨 상태로 시간 진화를 구현하십시오.\"\n",
        "---\n",
        "\n",
        "{/* cspell:ignore ncol circo Layerwise markersize unbiasedness infty ndash lesssim propto tenpy unfused Néel correlator Neel exponentiating gtrsim */}\n",
        "\n",
        "<span id=\"multi-product-formulas-to-reduce-trotter-error\" />\n",
        "\n",
        "# 트로터 오류를 줄이기 위한 다중 제품 공식\n",
        "\n",
        "*예상 소요 시간: Heron r2 프로세서에서 4분 (참고: 이는 단지 예상치일 뿐입니다.) (실행 시간은 다를 수 있습니다.)*\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "c4d0b2f2",
      "metadata": {},
      "source": [
        "<span id=\"learning-outcomes\" />\n",
        "\n",
        "## 학습 성과\n",
        "\n",
        "* 다중 제품 공식(MPF)이 여러 개의 얕은 회로에서 얻은 기대값을 결합함으로써 해밀토니안 시뮬레이션에서 트로터 오차를 어떻게 줄이는가\n",
        "* MPF가 표준 상품 구성보다 유리한 경우와 그렇지 않은 경우\n",
        "* 이 `qiskit_addon_mpf` 패키지를 사용하여 정적 및 동적 MPF 계수를 계산하는 방법\n",
        "* IBM Quantum® 하드웨어에서 트랜스파일링, 오류 완화 및 후처리를 포함한 MPF 워크플로를 처음부터 끝까지 실행하는 방법\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "f5dfd316",
      "metadata": {},
      "source": [
        "<span id=\"prerequisites\" />\n",
        "\n",
        "## 전제조건\n",
        "\n",
        "* [해밀턴 연산 시뮬레이션 회로의 조합 방법](/docs/tutorials/compilation-methods-for-hamiltonian-simulation-circuits) — Qiskit에서 트로터(곱 공식) 회로를 소개합니다.\n",
        "* [`SuzukiTrotter`](/docs/api/qiskit/qiskit.synthesis.SuzukiTrotter) Qiskit의 제품 공식, 특히 및 [`LieTrotter`](/docs/api/qiskit/qiskit.synthesis.LieTrotter) 합성 클래스.\n",
        "* [Qiskit primitives 그리고 Estimator 인터페이스](/docs/guides/primitives).\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "07273b26",
      "metadata": {},
      "source": [
        "<span id=\"background\" />\n",
        "\n",
        "## 배경\n",
        "\n",
        "<span id=\"what-are-multi-product-formulas\" />\n",
        "\n",
        "### 다중 제품 포뮬러란 무엇인가요?\n",
        "\n",
        "양자 컴퓨터에서 양자 시스템을 시뮬레이션할 때, 핵심 과제는 해밀토니안 $H$ 에 대한 시간 진화 연산자 $e^{-iHt}$ 를 근사화하는 것이다. 표준적인 접근 방식은 트로터-스즈키 분해라고도 알려진 *곱 공식* (PF)을 사용하는 것이다. 이 방법은 $H = \\sum_{a=1}^d F_a$ 를 개별 유니터리 연산 $e^{-iF_a t}$ 을 효율적으로 구현할 수 있는 항들로 분해한 다음, 전체 진화 과정을 이러한 더 단순한 유니터리 연산들의 순서된 곱으로 근사화합니다.\n",
        "\n",
        "1차 곱 공식(리-트로터)은 다음과 같습니다:\n",
        "\n",
        "$$\n",
        "S_1(t) := \\prod_{a=1}^d e^{-i F_a t},\n",
        "$$\n",
        "\n",
        "이는 2차 오차를 초래합니다: $S_1(t) = e^{-iHt} + \\mathcal{O}(t^2)$. 고차 대칭 공식 $S_{2\\chi}(t)$ (여기서 $\\chi$ 는 대칭 곱 공식의 차수를 나타냅니다 [\\[1\\]](#references) 참조)은 $e^{-iHt} + \\mathcal{O}(t^{2\\chi+1})$ 로 더 빠르게 수렴하지만, 단계당 회로 깊이가 더 깊어지는 대가를 치릅니다.\n",
        "\n",
        "*고정* 차수 $\\chi$ 에서의 오차를 줄이기 위해, 일반적으로 전체 진화 시간 $t$ 을 $k$ 개의 작은 트로터 단계로 나눕니다. 각 단계에서는 $e^{-iHt/k}$ 를 곱셈 공식으로 근사화하며, 이러한 단계들이 연결됩니다:\n",
        "\n",
        "$$\n",
        "e^{-iHt} \\approx \\left[S_{2\\chi}(t/k)\\right]^k.\n",
        "$$\n",
        "\n",
        "$2\\chi$ 차 대칭 공식의 경우, 잔여 트로터 오차는 $\\mathcal{O}\\!\\left(t^{2\\chi+1} / k^{2\\chi}\\right)$ 의 비율로 증가합니다. 따라서 $k$ 를 증가시키면 트로터 오차를 빠르게 억제할 수 있지만, 동시에 회로 깊이가 선형적으로 증가하게 되며, 노이즈가 많은 하드웨어에서는 이는 게이트 노이즈의 누적량이 더 많아진다는 것을 의미합니다. **트로터 오차( $k$ 가 클수록 유리함)** 와 **하드웨어 노이즈( $k$ 가 작을수록 유리함)** 사이의 이러한 상충 관계야말로, 다중 제품 공식이 해결하기 위해 고안된 바로 그 문제입니다. MPF는 고정된 순서 $\\chi$ 로 *$k$ 의 다양한 선택* 결과를 결합하는 것에 관한 것이며, 기본 곱셈 공식의 순서를 변경하는 것은 아니라는 점에 유의하십시오.\n",
        "\n",
        "**다중 제품 공식(MPF)** [\\[1\\]은](#references) 각각 서로 다른 수의 트로터 단계 $k_1, k_2, \\ldots, k_r$ ( $r$ 단계 수 집합)를 사용하는 여러 개의 더 얕은 트로터 회로에서 얻은 기대값의 *가중 선형 조합* 을 구성합니다:\n",
        "\n",
        "$$\n",
        "\\langle A \\rangle_{\\text{MPF}}(t) = \\sum_{j=1}^r x_j \\, \\langle A \\rangle_{k_j}(t),\n",
        "$$\n",
        "\n",
        "여기서 $\\langle A \\rangle_{k_j}(t)$ 는 시간 $t$ 에서 관측량 $A$ 의 기대값으로, $k_j$ 단계의 트로터 회로를 통해 추정된 것이며, 계수 $\\{x_j\\}_{j=1}^r$ 는 조합에서 주요 트로터 오차 항들이 상쇄되도록 선택된다. [4단계](#small-scale-step-4) 에서 이 식을 다시 살펴보면서, 트로터(Trotter)의 결과를 종합하기 위해 이를 명시적으로 계산할 것입니다. 실무상 가장 중요한 점은 MPF의 가장 깊은 회로에 필요한 단계 수가 $k_{\\max}$ 에 불과하다는 것인데, 이는 동일한 유효 트로터 오차를 직접 구하는 데 필요한 단일 $k$ 보다 훨씬 적은 수치입니다. 회로 깊이가 얕기 때문에 MPF 방식은 노이즈가 많은 하드웨어에 더 적합합니다.\n",
        "\n",
        "<span id=\"how-are-the-coefficients-determined\" />\n",
        "\n",
        "### 계수는 어떻게 결정되나요?\n",
        "\n",
        "MPF 계수에는 두 가지 계열이 있습니다:\n",
        "\n",
        "**정적 계수는** 해밀토니안, 초기 상태 및 진화 시간과 무관하다. 이는 선행 트로터 오차 항들이 상쇄되도록 하는 선형 연립방정식 $Ax = b$ 을 풀어서 구할 수 있다. $2\\chi$ 차 대칭 곱 공식과 함께 사용되는 일련의 트로터 단계 $\\{k_j\\}_{j=1}^r$ 에 대해, $k_j$ 의 역수 제곱으로 트로터 오차를 전개하면 다음과 같은 형태의 제약 방정식이 도출된다:\n",
        "\n",
        "$$\n",
        "\\sum_{j=1}^r x_j = 1, \\quad \\sum_{j=1}^r \\frac{x_j}{k_j^{\\eta_n}} = 0 \\quad (n = 0, \\ldots, r-2),\n",
        "$$\n",
        "\n",
        "여기서 정수 지수 $\\{\\eta_n\\}$ 는 선택된 곱셈 공식에 대한 연속적인 트로터 오차 항들의 차수를 나타낸다. *대칭적인*$2\\chi$ 차 PF의 경우, $\\left[S_{2\\chi}(t/k)\\right]^k$ 의 주 오차는 $1/k^{2\\chi}$ 의 비율을 따르며, 후속 보정은 $1/k^{2\\chi+2}, 1/k^{2\\chi+4}, \\ldots$ 의 비율을 따릅니다. 따라서 지수는 $\\eta_n = 2\\chi + 2n$ 입니다. 비대칭 PF의 경우, 홀수 지수와 짝수 지수가 모두 기여하며 $\\eta_n = 2\\chi + n$ 입니다. 전체 도출 과정은 참고문헌 [\\[1\\]](#references) 을 참조하십시오. 위 방정식계의 첫 번째 방정식은 편향 없음( $k_j \\to \\infty$ 한 극한에서 MPF가 정확한 기대값을 재현함)을 보장하며, 나머지 $r-1$ 방정식들은 첫 번째 $r-1$ 트로터 오차 항들을 차례로 상쇄합니다. 결과로 얻어진 $L_1$ -노름 $\\|x\\|_1$ 이 너무 클 경우(이는 표본 잡음을 증폭시킴), 대신 $\\|x\\|_1$ 을 상한으로 설정하고 $\\|Ax - b\\|$ 을 최소화하는 근사 최적화 문제를 풀 수 있습니다.\n",
        "\n",
        "**동적 계수** [\\[2\\]](#references), [\\[3\\]은](#references) 또한 해밀토니안, 초기 상태 및 진화 시간 $t$ 에 따라 달라진다. 이 계수들은 실제 시간 진화 상태와 MPF 근사치 사이의 프로베니우스 노름 거리를 최소화한다:\n",
        "\n",
        "$$\n",
        "\\|\\rho(t) - \\mu^D(t)\\|_F^2 = 1 + \\sum_{i,j} M_{ij}(t)\\, x_i(t)\\, x_j(t) - 2\\sum_i L_i(t)\\, x_i(t),\n",
        "$$\n",
        "\n",
        "여기서 $M_{ij}(t) = \\mathrm{Tr}[\\rho_{k_i}(t)\\,\\rho_{k_j}(t)]$ 는 서로 다른 단계 수 $k_i, k_j$ 에 대한 트로터 진화 상태들 간의 중첩을 나타내는 그램 행렬이며, $L_i(t) = \\mathrm{Tr}[\\rho(t)\\,\\rho_{k_i}(t)]$ 는 (근사적인) 정확한 상태와의 중첩을 측정한다. `qiskit_addon_mpf`이 튜토리얼에서는 텐서 네트워크 기법, 특히 의 TeNPy-based 백엔드를 사용하여 이러한 수치를 효율적으로 계산합니다.\n",
        "\n",
        "<span id=\"when-to-use-mpfs\" />\n",
        "\n",
        "### MPF는 언제 사용해야 할까요?\n",
        "\n",
        "MPF는 다음과 같은 경우에 가장 큰 이점을 제공합니다:\n",
        "\n",
        "* **회로 깊이가 병목 지점입니다.** 하드웨어 노이즈로 인해 실행 가능한 깊이가 제한되는 경우, MPF를 사용하여 더 얕은 회로에서도 더 높은 유효 트로터 정확도를 달성하십시오.\n",
        "* **완전한 상태 준비가 아니라 정확한 기대값이 필요합니다.** MPF는 기대값 수준에서 작동합니다. 즉, 양자 상태가 아닌 고전적인 수치를 결합합니다. 따라서 Estimator 프리미티브를 사용할 때 관측 가능한 추정값을 구하는 데 이상적입니다.\n",
        "* **트로터 스텝 횟수를 적당히 조합합니다.** 일반적으로 $r = 3$ – $5$ 의 서로 다른 단계 수 $k_j$ 를 조합하면, $\\|x\\|_1$ 를 관리 가능한 수준으로 유지하면서 몇 가지 주요 트로터 오차 항을 상쇄하는 데 충분합니다.\n",
        "\n",
        "<span id=\"when-mpfs-might-not-help\" />\n",
        "\n",
        "### MPF가 도움이 되지 않을 수 있는 경우\n",
        "\n",
        "* **진화 시간이 매우 짧습니다.** $t$ 가 충분히 작아서 단일 저차 트로터 공식만으로도 이미 정확도가 보장되는 경우, 여러 회로를 실행하는 데 드는 부하가 불필요해집니다.\n",
        "* **상태 준비 작업.** MPF는 보정된 양자 상태가 아니라 보정된 *기대값* 을 산출합니다. 실제 시간 경과에 따른 상태(예를 들어, 다른 양자 서브루틴의 입력으로 사용하기 위한 경우)가 필요한 경우에는 MPF를 적용할 수 없습니다.\n",
        "* **수렴 조건을 위반하는 트로터 걸음 수.** 정적 계수 도출 과정에서는 각 개별 $\\left[S_{2\\chi}(t/k_j)\\right]^{k_j}$ 를 $t/k_j$ 의 급수로 전개하는데, 이 전개는 $t/k_{\\min} \\lesssim 1$ 일 때만 잘 수렴한다. 주어진 $t$ 에 비해 $k_{\\min}$ 를 너무 작게 선택하면, 가장 얕은 회로가 섭동 영역을 훨씬 벗어나게 되며, MPF가 상쇄하지 못한 고차 오차 항이 커지게 되고, 상쇄를 위해 큰 계수가 필요할 수 있다. $L_1$ -노름 $\\|x\\|_1$ 은 실용적인 진단 기준이 됩니다. 즉, $\\|x\\|_1 \\gg 1$ 일 때, 샘플링 오버헤드 $\\propto \\|x\\|_1^2$ 가 Trotter 오차 감소 효과를 상쇄할 수 있습니다. 자세한 내용은 [트로터 계단 선택](https://qiskit.github.io/qiskit-addon-mpf/how_tos/choose_trotter_steps.html) 가이드를 참조하세요.\n",
        "\n",
        "<span id=\"what-this-tutorial-covers\" />\n",
        "\n",
        "### 이 튜토리얼에서 다루는 내용\n",
        "\n",
        "이 튜토리얼에서는 MPF 워크플로우의 전 과정을 두 단계에 걸쳐 단계별로 안내합니다. 먼저, **소규모 시뮬레이터 예제** (10-큐비트 하이젠베르크 사슬)를 통해 문제를 설정하고, 정적 및 동적 MPF 계수를 계산하며, 그 결과로 얻은 기대값을 정확한 대각화 결과와 비교하는 방법을 보여줍니다. 이어서, **대규모 하드웨어 예제** (50-큐비트 XXZ 체인)를 통해 트랜스파일링 방법, 오류 완화 기능을 갖춘 IBM Quantum 하드웨어에서 실행하는 방법, 그리고 MPF 계수를 사용하여 결과를 후처리하는 방법을 보여줍니다. 이 문서 전반에 걸쳐 표준 Qiskit 도구와 함께 이 `qiskit_addon_mpf` 패키지를 사용합니다.\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "b5d478ce",
      "metadata": {},
      "source": [
        "<span id=\"requirements\" />\n",
        "\n",
        "## 요구사항\n",
        "\n",
        "이 튜토리얼을 시작하기 전에 다음이 설치되어 있는지 확인하세요:\n",
        "\n",
        "* Qiskit SDK v2.0 또는 그 이후 버전, [시각화](/docs/api/qiskit/visualization) 기능 지원\n",
        "* Qiskit Runtime v0.22 이상 (`pip install qiskit-ibm-runtime`)\n",
        "* Qiskit Aer 시뮬레이터 (`pip install qiskit-aer`)\n",
        "* TeNPy 백엔드를 사용하는 MPF Qiskit 애드온 (`pip install \"qiskit-addon-mpf[tenpy]\"`)\n",
        "* Qiskit 애드온 유틸리티 (`pip install qiskit-addon-utils`)\n",
        "* SciPy (`pip install scipy`)\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "2584c37e",
      "metadata": {},
      "source": [
        "<span id=\"setup\" />\n",
        "\n",
        "## 설정\n",
        "\n",
        "아래에서는 이 튜토리얼 전반에 걸쳐 사용된 *모든* 패키지 임포트 문들을 하나의 셀에 모아 놓았습니다. `XXPlusYYGate`또한 인접한 `rxx` 및 `ryy` 회전을 하나의 로 병합하는 트랜스파일러 패스를 `CollectAndCollapse` 정의합니다. 이 처리는 1단계에서 회로를 구성할 때(게이트 수를 적게 유지하기 위해)뿐만 아니라, 4단계에서 동적 MPF를 위한 계층적 구조를 추출할 때도 간접적으로 적용됩니다( TeNPy 는 융합되지 않은 회전 쌍이 아닌 2-큐비트 게이트를 기대하기 때문입니다).\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 1,
      "id": "bf79f9e7",
      "metadata": {},
      "outputs": [],
      "source": [
        "import warnings\n",
        "\n",
        "import numpy as np\n",
        "import matplotlib.pyplot as plt\n",
        "from functools import partial\n",
        "from copy import deepcopy\n",
        "\n",
        "from qiskit import QuantumCircuit\n",
        "from qiskit.quantum_info import Pauli, SparsePauliOp, Statevector\n",
        "from qiskit.synthesis import SuzukiTrotter\n",
        "from qiskit.transpiler import CouplingMap, PassManager\n",
        "from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager\n",
        "from qiskit.circuit.library import XXPlusYYGate\n",
        "from qiskit.transpiler.passes.optimization.collect_and_collapse import (\n",
        "    CollectAndCollapse,\n",
        "    collect_using_filter_function,\n",
        "    collapse_to_operation,\n",
        ")\n",
        "\n",
        "from qiskit_aer import AerSimulator\n",
        "from qiskit_ibm_runtime import EstimatorV2 as Estimator, QiskitRuntimeService\n",
        "\n",
        "from qiskit_addon_utils.problem_generators import (\n",
        "    generate_xyz_hamiltonian,\n",
        "    generate_time_evolution_circuit,\n",
        ")\n",
        "from qiskit_addon_utils.slicing import slice_by_depth\n",
        "from qiskit_addon_mpf.static import setup_static_lse\n",
        "from qiskit_addon_mpf.dynamic import setup_dynamic_lse\n",
        "from qiskit_addon_mpf.costs import (\n",
        "    setup_exact_problem,\n",
        "    setup_sum_of_squares_problem,\n",
        "    setup_frobenius_problem,\n",
        ")\n",
        "from qiskit_addon_mpf.backends.tenpy_layers import (\n",
        "    LayerModel,\n",
        "    LayerwiseEvolver,\n",
        ")\n",
        "from qiskit_addon_mpf.backends.tenpy_tebd import MPOState, MPS_neel_state\n",
        "\n",
        "from scipy.linalg import expm\n",
        "\n",
        "# Suppress TeNPy's `unit_cell_width` future-API warning. The default\n",
        "# (`unit_cell_width=len(sites)`) is correct for Chain lattices, which is what\n",
        "# `CouplingMap.from_line(...)` produces here, so the warning is informational.\n",
        "warnings.filterwarnings(\n",
        "    \"ignore\",\n",
        "    message=r\".*unit_cell_width.*\",\n",
        "    category=UserWarning,\n",
        ")\n",
        "\n",
        "\n",
        "# --- Helper: collect XX + YY rotations into a single gate ---\n",
        "def filter_function(node):\n",
        "    return node.op.name in {\"rxx\", \"ryy\"}\n",
        "\n",
        "\n",
        "collect_function = partial(\n",
        "    collect_using_filter_function,\n",
        "    filter_function=filter_function,\n",
        "    split_blocks=True,\n",
        "    min_block_size=1,\n",
        ")\n",
        "\n",
        "\n",
        "def collapse_to_xx_plus_yy(block):\n",
        "    param = 0.0\n",
        "    for node in block.data:\n",
        "        param += node.operation.params[0]\n",
        "    return XXPlusYYGate(param)\n",
        "\n",
        "\n",
        "collapse_function = partial(\n",
        "    collapse_to_operation,\n",
        "    collapse_function=collapse_to_xx_plus_yy,\n",
        ")\n",
        "\n",
        "pm = PassManager()\n",
        "pm.append(CollectAndCollapse(collect_function, collapse_function))"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "24f08467",
      "metadata": {},
      "source": [
        "<span id=\"small-scale-simulator-example\" />\n",
        "\n",
        "## 소규모 시뮬레이터 예시\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "378e82ba",
      "metadata": {},
      "source": [
        "<span id=\"step-1-map-classical-inputs-to-a-quantum-problem\" />\n",
        "\n",
        "### 1단계: 고전적 입력을 양자 문제에 매핑하기\n",
        "\n",
        "먼저, 네엘 상태 $\\vert 0101\\ldots01 \\rangle$ 를 초기 상태로 삼아, 직선 상의 10-큐비트 하이젠베르크 모델을 고려합니다. 해밀토니안은 다음과 같습니다:\n",
        "\n",
        "$$\n",
        "\\hat{\\mathcal{H}}_{\\text{Heis}} = J \\sum_{i=1}^{L-1} \\left(X_i X_{i+1} + Y_i Y_{i+1} + Z_i Z_{i+1}\\right),\n",
        "$$\n",
        "\n",
        "여기서 $J$ 는 최인접 이웃 결합 강도이다. 우리는 체인의 중간에 위치한 한 쌍의 큐비트에 대해 ZZ 상관함수 $Z_{L/2-1} Z_{L/2}$ 를 측정하고, 2차 곱 공식 $k_j = [1, 2, 4]$ 을 적용한 트로터 단계를 사용합니다.\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 2,
      "id": "bdd0d4fc",
      "metadata": {},
      "outputs": [
        {
          "name": "stdout",
          "output_type": "stream",
          "text": [
            "SparsePauliOp(['IIIIIIIXXI', 'IIIIIIIYYI', 'IIIIIIIZZI', 'IIIIIXXIII', 'IIIIIYYIII', 'IIIIIZZIII', 'IIIXXIIIII', 'IIIYYIIIII', 'IIIZZIIIII', 'IXXIIIIIII', 'IYYIIIIIII', 'IZZIIIIIII', 'IIIIIIIIXX', 'IIIIIIIIYY', 'IIIIIIIIZZ', 'IIIIIIXXII', 'IIIIIIYYII', 'IIIIIIZZII', 'IIIIXXIIII', 'IIIIYYIIII', 'IIIIZZIIII', 'IIXXIIIIII', 'IIYYIIIIII', 'IIZZIIIIII', 'XXIIIIIIII', 'YYIIIIIIII', 'ZZIIIIIIII'],\n",
            "              coeffs=[1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j,\n",
            " 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j,\n",
            " 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j])\n"
          ]
        }
      ],
      "source": [
        "L = 10\n",
        "\n",
        "# Generate coupling map and Hamiltonian\n",
        "coupling_map = CouplingMap.from_line(L, bidirectional=False)\n",
        "\n",
        "hamiltonian = generate_xyz_hamiltonian(\n",
        "    coupling_map,\n",
        "    coupling_constants=(1.0, 1.0, 1.0),\n",
        "    ext_magnetic_field=(0.0, 0.0, 0.0),\n",
        ")\n",
        "print(hamiltonian)"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 3,
      "id": "fd3dc9c8",
      "metadata": {},
      "outputs": [
        {
          "name": "stdout",
          "output_type": "stream",
          "text": [
            "SparsePauliOp(['IIIIZZIIII'],\n",
            "              coeffs=[1.+0.j])\n"
          ]
        }
      ],
      "source": [
        "# Observable: ZZ on the middle pair of qubits\n",
        "observable = SparsePauliOp.from_sparse_list(\n",
        "    [(\"ZZ\", (L // 2 - 1, L // 2), 1.0)], num_qubits=L\n",
        ")\n",
        "print(observable)"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 4,
      "id": "398c33b2",
      "metadata": {},
      "outputs": [],
      "source": [
        "# MPF parameters\n",
        "mpf_trotter_steps = [1, 2, 4]\n",
        "order = 2\n",
        "symmetric = False\n",
        "\n",
        "trotter_times = np.arange(0.5, 1.55, 0.1)\n",
        "exact_evolution_times = np.arange(trotter_times[0], 1.55, 0.05)"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "1512ba0e",
      "metadata": {},
      "source": [
        "<span id=\"build-trotter-circuits\" />\n",
        "\n",
        "#### 트로터 회로 만들기\n",
        "\n",
        "우리는 각 시간점과 각 트로터 단계 수에 대해 근사 트로터 시간 진화를 구현하는 회로를 생성합니다. ‘설정’ 섹션에서 정의된 이 `CollectAndCollapse` 패스는 XX 및 YY 회전을 단일 XX+YY 게이트로 통합하여, 이후 더 효율적인 텐서 네트워크 시뮬레이션을 준비합니다.\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 5,
      "id": "1c194d2b",
      "metadata": {},
      "outputs": [],
      "source": [
        "# Initial Neel state preparation\n",
        "initial_state_circ = QuantumCircuit(L)\n",
        "initial_state_circ.x([i for i in range(L) if i % 2 != 0])\n",
        "\n",
        "\n",
        "all_circs = []\n",
        "for total_time in trotter_times:\n",
        "    mpf_trotter_circs = [\n",
        "        generate_time_evolution_circuit(\n",
        "            hamiltonian,\n",
        "            time=total_time,\n",
        "            synthesis=SuzukiTrotter(reps=num_steps, order=order),\n",
        "        )\n",
        "        for num_steps in mpf_trotter_steps\n",
        "    ]\n",
        "\n",
        "    mpf_trotter_circs = pm.run(\n",
        "        mpf_trotter_circs\n",
        "    )  # Collect XX and YY into XX + YY\n",
        "\n",
        "    mpf_circuits = [\n",
        "        initial_state_circ.compose(circuit) for circuit in mpf_trotter_circs\n",
        "    ]\n",
        "    all_circs.append(mpf_circuits)"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 6,
      "id": "c7ee61e7",
      "metadata": {},
      "outputs": [
        {
          "data": {
            "text/plain": [
              "<Image src=\"/docs/images/tutorials/multi-product-formula/extracted-outputs/c7ee61e7-0.avif\" alt=\"Output of the previous code cell\" />"
            ]
          },
          "execution_count": 6,
          "metadata": {},
          "output_type": "execute_result"
        }
      ],
      "source": [
        "mpf_circuits[-1].draw(\"mpl\", fold=-1)"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "4cd6c782",
      "metadata": {},
      "source": [
        "<span id=\"step-2-optimize-problem-for-quantum-hardware-execution\" />\n",
        "\n",
        "### 2단계: 양자 하드웨어 실행을 위한 문제 최적화\n",
        "\n",
        "소규모 예제로는 Aer 시뮬레이터를 다룰 것입니다. 회로가 실행 준비가 되기 전에 두 가지 변환이 이루어집니다:\n",
        "\n",
        "1. **해밀토니안 시뮬레이션 단계에서의 게이트 수집.** `XXPlusYYGate`‘Setup’ 셀에서는 인접한 `rxx` 및 `ryy` 회전을 하나의 로 통합하는 패스를 `CollectAndCollapse` 구축했습니다. 우리는 1단계(호출 `pm.run(...)` )에서 트로터 회로를 구축할 때 이미 이 단계를 적용했습니다. 이를 통해 2-큐비트 게이트의 수를 줄일 수 있을 뿐만 아니라, 이후 동적 계수 계산을 위한 텐서 네트워크 시뮬레이션에 더 적합한 구조를 얻을 수 있다.\n",
        "\n",
        "2. **시뮬레이터의 ISA로 전환합니다.** 아래에서는 Qiskit 프리셋 패스 매니저를 실행하여 `optimization_level=3` 각 Trotter 회로를 시뮬레이터의 명령어 집합 아키텍처(ISA) 수준으로 변환합니다.\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 7,
      "id": "03590a05",
      "metadata": {},
      "outputs": [],
      "source": [
        "aer_sim = AerSimulator()\n",
        "pm_sim = generate_preset_pass_manager(backend=aer_sim, optimization_level=3)\n",
        "\n",
        "isa_circs_all_times = [\n",
        "    pm_sim.run([deepcopy(c) for c in mpf_circuits])\n",
        "    for mpf_circuits in all_circs\n",
        "]"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "ab6588d3",
      "metadata": {},
      "source": [
        "<span id=\"step-3-execute-using-qiskit-primitives\" />\n",
        "\n",
        "### 3단계: `Qiskit primitives` 명령어로 실행합니다\n",
        "\n",
        "소규모 예제의 경우, ISA로 변환된 트로터 회로를 Aer가 지원하는 프리미티브를 `EstimatorV2` 통해 실행합니다. 이렇게 하면 각 $(k_j, t)$ 쌍에 대해 *잡음이 없는* 기준값을 얻을 수 있습니다. 이 값들이 바로 MPF가 4단계에서 결합할 $\\langle A \\rangle_{k_j}(t)$ 값들입니다. 나중에 각 개별 제품 공식과 MPF의 전체 시계열 곡선을 그래프로 나타낼 수 있도록 진화 시점을 훑어봅니다.\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 8,
      "id": "7225d782",
      "metadata": {},
      "outputs": [],
      "source": [
        "estimator = Estimator(mode=aer_sim)\n",
        "\n",
        "mpf_expvals_all_times, mpf_stds_all_times = [], []\n",
        "for isa_circuits in isa_circs_all_times:\n",
        "    result = estimator.run(\n",
        "        [(circuit, observable) for circuit in isa_circuits], precision=0.005\n",
        "    ).result()\n",
        "    mpf_expvals_all_times.append([res.data.evs for res in result])\n",
        "    mpf_stds_all_times.append([res.data.stds for res in result])"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "a384f017",
      "metadata": {},
      "source": [
        "<span id=\"small-scale-step-4\" />\n",
        "\n",
        "<span id=\"step-4-post-process-and-return-result-in-desired-classical-format\" />\n",
        "\n",
        "### 4단계: 후처리 수행 및 원하는 클래식 형식으로 결과 반환\n",
        "\n",
        "4단계에서는 MPF가 실제로 구축됩니다. 비록 여기서는 계수 $x_j$\\* 가\\* 계산되지만(동적 변형의 경우 이 계산이 상당한 연산 부하를 유발할 수 있음), 개념적으로 이는 3단계의 양자 측정 결과를 하나의 보정된 기대값으로 결합하는 고전적인 방법이기 때문에, 우리는 전체 계수 및 결합 작업 흐름을 후처리 단계로 간주합니다.\n",
        "\n",
        "MPF가 실제 동역학을 얼마나 잘 재현하는지 평가하기 위해, 먼저 해밀토니안을 직접 지수 함수화하여 시간 경과에 따른 정확한 기대값을 계산한다. 이 문제가 해결 가능한 이유는 $L = 10$ 이기 때문이며, 아래의 대규모 하드웨어 예시에서는 대신 텐서 네트워크 추정값에 의존해야 할 것입니다.\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 9,
      "id": "a223a360",
      "metadata": {},
      "outputs": [],
      "source": [
        "exact_expvals = []\n",
        "for t in exact_evolution_times:\n",
        "    exp_H = expm(-1j * t * hamiltonian.to_matrix())\n",
        "    initial_state = Statevector(initial_state_circ).data\n",
        "    time_evolved_state = exp_H @ initial_state\n",
        "\n",
        "    exact_obs = (\n",
        "        time_evolved_state.conj()\n",
        "        @ observable.to_matrix()\n",
        "        @ time_evolved_state\n",
        "    ).real\n",
        "    exact_expvals.append(exact_obs)"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "361e726e",
      "metadata": {},
      "source": [
        "<span id=\"static-mpf-coefficients\" />\n",
        "\n",
        "#### 정적 MPF 계수\n",
        "\n",
        "정적 MPF는 진화 시간, 해밀토니안 및 초기 상태와 무관한 계수 $x_j$ 를 사용합니다. 배경에서 설명한 선형 방정식 시스템 $Ax = b$ 을 세우고, 계수를 구합니다. 행렬 $A$ 은 트로터 단계 수 $k_j$, 곱 공식의 차수 $\\chi$, 그리고 공식이 대칭인지 여부(이는 지수 $\\eta_n$ 를 결정함)에 의해 결정된다.\n",
        "\n",
        "이 소규모 예제에서는 비대칭 차수( $2\\chi=2$ )의 스즈키-트로터 공식(따라서 $\\chi=1$ 및 $\\eta_n = 2 + n$, 결과적으로 $\\eta_0 = 2,\\, \\eta_1 = 3$ )을 적용한 $k_j = [1, 2, 4]$ 를 사용합니다. 이에 따라 시스템은 다음과 같이 표현됩니다:\n",
        "\n",
        "$$\n",
        "A =\n",
        "\\begin{bmatrix}\n",
        "1 & 1 & 1\\\\\n",
        "1 & \\frac{1}{2^2} & \\frac{1}{4^2}  \\\\\n",
        "1 & \\frac{1}{2^3} & \\frac{1}{4^3}  \\\\\n",
        "\\end{bmatrix}, \\quad\n",
        "b =\n",
        "\\begin{bmatrix}\n",
        "1 \\\\\n",
        "0 \\\\\n",
        "0\n",
        "\\end{bmatrix}.\n",
        "$$\n",
        "\n",
        "첫 번째 행은 편향 없음( $\\sum_j x_j = 1$ )을 보장하며, 두 번째와 세 번째 행은 각각 선행 트로터 오차 항 $1/k^2$ 과 차순위 트로터 오차 항 $1/k^3$ 을 상쇄합니다.\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "4f2ca1e2",
      "metadata": {},
      "source": [
        "<span id=\"set-up-the-lse\" />\n",
        "\n",
        "##### LSE 설정하기\n",
        "\n",
        "우리는 에서 `qiskit_addon_mpf.static` 를 사용하여 `setup_static_lse` 앞서 설명한 행렬 $A$ 과 우변 벡터 $b$ 를 구성합니다. 행렬 $A$ 은 $k_j$ 뿐만 아니라, 곱의 공식 선택 — 특히 그 *차수* $\\chi$ 와 *대칭* 여부 — 에 따라서도 달라집니다. 이 `symmetric` 플래그는 지수 패턴 $\\eta_n$ 을 제어합니다(대칭 식은 짝수 차수의 트로터 오차 항만을 생성합니다; 참고문헌 [\\[1\\]](#references) 참조). 참고 문헌 [\\[2\\]](#references) 에 제시된 바와 같이, 기본 PF가 대칭적일 때조차 를 설정하는 `symmetric=True` 것이 엄밀히 말해 반드시 필요한 것은 아니라는 점에 유의해야 한다. 비대칭 LSE는 여전히 유효하며(단, 불필요한 추가 제약 조건을 부과할 뿐이다).\n",
        "\n",
        "이 예제에서는 1단계에서 이미 와 `symmetric = False` 를 설정해 `order = 2` 두었습니다.\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 10,
      "id": "827b0b42",
      "metadata": {},
      "outputs": [],
      "source": [
        "lse = setup_static_lse(mpf_trotter_steps, order=order, symmetric=symmetric)"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "003e4bdd",
      "metadata": {},
      "source": [
        "구성된 행렬 $A$ 과 벡터 $b$ 을 검토하여, 이들이 위에 기술된 시스템과 일치하는지 확인하십시오.\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 11,
      "id": "6f879978",
      "metadata": {},
      "outputs": [
        {
          "data": {
            "text/plain": [
              "array([[1.      , 1.      , 1.      ],\n",
              "       [1.      , 0.25    , 0.0625  ],\n",
              "       [1.      , 0.125   , 0.015625]])"
            ]
          },
          "execution_count": 11,
          "metadata": {},
          "output_type": "execute_result"
        }
      ],
      "source": [
        "lse.A"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 12,
      "id": "64fe7db9",
      "metadata": {},
      "outputs": [
        {
          "data": {
            "text/plain": [
              "array([1., 0., 0.])"
            ]
          },
          "execution_count": 12,
          "metadata": {},
          "output_type": "execute_result"
        }
      ],
      "source": [
        "lse.b"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "16ab9f79",
      "metadata": {},
      "source": [
        "LSE를 바탕으로, ( $x = A^{-1}b$ 의 직접 해법)을 통해 `lse.solve()` 정적 계수 $x_j$ 를 구합니다.\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 13,
      "id": "7b69192a",
      "metadata": {},
      "outputs": [
        {
          "name": "stdout",
          "output_type": "stream",
          "text": [
            "The static coefficients associated with the ansatze are: [ 0.04761905 -0.57142857  1.52380952]\n"
          ]
        }
      ],
      "source": [
        "mpf_coeffs = lse.solve()\n",
        "print(\n",
        "    f\"The static coefficients associated with the ansatze are: {mpf_coeffs}\"\n",
        ")"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "1c238a59",
      "metadata": {},
      "source": [
        "<span id=\"optimize-for-$x$-using-an-exact-model\" />\n",
        "\n",
        "##### 정확한 모델을 사용하여 $x$ 에 최적화하십시오\n",
        "\n",
        "$x = A^{-1}b$ 을 계산하는 대신, [setup\\_exact\\_model을](https://qiskit.github.io/qiskit-addon-mpf/stubs/qiskit_addon_mpf.static.setup_exact_model.html) 사용하여 LSE를 제약 조건으로 삼고, 그 최적 해가 $x$ 을 산출하는 [cvxpy.Problem](https://www.cvxpy.org/api_reference/cvxpy.problems.html#cvxpy.Problem) 인스턴스를 생성할 수 있습니다.\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 14,
      "id": "993465e9",
      "metadata": {},
      "outputs": [
        {
          "name": "stdout",
          "output_type": "stream",
          "text": [
            "[ 0.04761905 -0.57142857  1.52380952]\n"
          ]
        }
      ],
      "source": [
        "model_exact, coeffs_exact = setup_exact_problem(lse)\n",
        "model_exact.solve()\n",
        "print(coeffs_exact.value)"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 15,
      "id": "eb61ea70",
      "metadata": {},
      "outputs": [
        {
          "name": "stdout",
          "output_type": "stream",
          "text": [
            "L1 norm of the exact coefficients: 2.1428571428556378\n"
          ]
        }
      ],
      "source": [
        "print(\n",
        "    \"L1 norm of the exact coefficients:\",\n",
        "    np.linalg.norm(coeffs_exact.value, ord=1),\n",
        ")"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "dac0472b",
      "metadata": {},
      "source": [
        "<span id=\"optimize-for-$x$-using-an-approximate-model\" />\n",
        "\n",
        "##### $x$ 에 대한 근사 모델을 사용한 최적화\n",
        "\n",
        "선택된 $k_j$ 값 집합에 대한 $L_1$ 규범이 너무 높다고 판단될 수도 있습니다. 만약 그런 상황이고 다른 $k_j$ 값을 선택할 수 없다면, $\\|Ax - b\\|$ 을 최소화하면서 $L_1$ -노름을 선택한 임계값으로 제한하는 근사 해법을 사용할 수 있습니다. [‘근사 모델 사용](https://qiskit.github.io/qiskit-addon-mpf/how_tos/using_approximate_model.html) 방법’ 가이드를 확인해 보세요.\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 16,
      "id": "0cd7dea4",
      "metadata": {},
      "outputs": [
        {
          "name": "stdout",
          "output_type": "stream",
          "text": [
            "[-1.10294118e-03 -2.48897059e-01  1.25000000e+00]\n",
            "L1 norm of the approximate coefficients: 1.5\n"
          ]
        }
      ],
      "source": [
        "model_approx, coeffs_approx = setup_sum_of_squares_problem(\n",
        "    lse, max_l1_norm=1.5\n",
        ")\n",
        "model_approx.solve()\n",
        "print(coeffs_approx.value)\n",
        "print(\n",
        "    \"L1 norm of the approximate coefficients:\",\n",
        "    np.linalg.norm(coeffs_approx.value, ord=1),\n",
        ")"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "10fd05eb",
      "metadata": {},
      "source": [
        "<span id=\"dynamic-mpf-coefficients\" />\n",
        "\n",
        "#### 동적 MPF 계수\n",
        "\n",
        "정적 MPF는 해밀토니안 및 상태에 구애받지 않는 방식으로 트로터 오차 항을 상쇄하므로, 주어진 해밀토니안과 초기 상태에 대해 반드시 가능한 한 가장 작은 근사 오차를 산출하는 것은 아니다. `qiskit_addon_mpf`반면, 동적 MPF(참고문헌 [\\[2\\]](#references), [\\[3\\]](#references) )는 각 시간 $t$ 에서 프로베니우스 노름 거리 $\\|\\rho(t) - \\mu^D(t)\\|_F^2$ 를 최소화하는 시간 의존적 계수 $x_i(t)$ 를 구합니다. ‘배경’ 절에서 설명한 바와 같이, 이를 위해서는 트로터 진화 상태들 간의 중첩 행렬 $M_{ij}(t)$ 과 정확한 상태와의 중첩 $L_i(t)$ 이 필요하며, 이 두 가지 모두 본 논문에서 텐서 네트워크( TeNPy ) 백엔드를 사용하여 추정합니다.\n",
        "\n",
        "동적 LSE를 설정하려면 다음 세 가지 요소가 필요합니다:\n",
        "\n",
        "1. 이 애드온이 각 $k_j$ 에 대해 실행하여 $\\rho_{k_j}(t)$ 를 MPS/MPO 형식으로 생성하는 **근사적 에볼버 팩토리입니다**. `slice_by_depth`우리는 순서- $2$ 트로터 회로의 층별 구조(각 층당 하나씩)를 바탕으로 이를 구축하며, 이를 TeNPy 의 절단 매개변수를 갖는 형태로 `LayerwiseEvolver` 감싸서 구성합니다.\n",
        "2. 고정밀도 기준 $\\rho(t)$ 를 생성하는 **정확한 진화기 팩토리입니다**. 우리는 정확한 진화를 대리하기 위해 작은 시간 단계의 4차 스즈키-트로터 회로(`dt=0.1`, `order=4`)를 사용합니다.\n",
        "3. TeNPy 시뮬레이션에 초기값을 제공하는 **아이덴티티 팩토리와** **초기 상태 MPS입니다**.\n",
        "\n",
        "아래 코드는 근사적 진화 알고리즘 생성기를 구현합니다.\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 17,
      "id": "52d78403",
      "metadata": {},
      "outputs": [],
      "source": [
        "# Create approximate time-evolution circuits\n",
        "single_2nd_order_circ = generate_time_evolution_circuit(\n",
        "    hamiltonian, time=1.0, synthesis=SuzukiTrotter(reps=1, order=order)\n",
        ")\n",
        "single_2nd_order_circ = pm.run(single_2nd_order_circ)  # collect XX and YY\n",
        "\n",
        "# Find layers in the circuit\n",
        "layers = slice_by_depth(single_2nd_order_circ, max_slice_depth=1)\n",
        "\n",
        "# Create tensor network models\n",
        "models = [\n",
        "    LayerModel.from_quantum_circuit(layer, conserve=\"Sz\") for layer in layers\n",
        "]\n",
        "\n",
        "# Create the time-evolution object\n",
        "approx_factory = partial(\n",
        "    LayerwiseEvolver,\n",
        "    layers=models,\n",
        "    options={\n",
        "        \"preserve_norm\": False,\n",
        "        \"trunc_params\": {\n",
        "            \"chi_max\": 64,\n",
        "            \"svd_min\": 1e-8,\n",
        "            \"trunc_cut\": None,\n",
        "        },\n",
        "        \"max_delta_t\": 2,\n",
        "    },\n",
        ")"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "91d2783e",
      "metadata": {},
      "source": [
        "<Admonition type=\"warning\">\n",
        "  텐서 네트워크 시뮬레이션의 세부 사항을 결정하는 `LayerwiseEvolver` 의 옵션은 잘못 정의된 최적화 문제를 설정하지 않도록 신중하게 선택해야 합니다.\n",
        "</Admonition>\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "1c702f67",
      "metadata": {},
      "source": [
        "`dt=0.1`우리는 작은 시간 간격을 사용하여 4차 스즈키-트로터 공식을 통해 정확한 시간 진화 상태를 근사화한다. TeNPy 의 트런케이션 매개변수는 정확도에 영향을 미칠 수 있으므로, 다양한 값을 시도해 보는 것이 중요합니다.\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 18,
      "id": "abab8bfc",
      "metadata": {},
      "outputs": [],
      "source": [
        "single_4th_order_circ = generate_time_evolution_circuit(\n",
        "    hamiltonian, time=1.0, synthesis=SuzukiTrotter(reps=1, order=4)\n",
        ")\n",
        "single_4th_order_circ = pm.run(single_4th_order_circ)\n",
        "exact_model_layers = [\n",
        "    LayerModel.from_quantum_circuit(layer, conserve=\"Sz\")\n",
        "    for layer in slice_by_depth(single_4th_order_circ, max_slice_depth=1)\n",
        "]\n",
        "\n",
        "exact_factory = partial(\n",
        "    LayerwiseEvolver,\n",
        "    layers=exact_model_layers,\n",
        "    dt=0.1,\n",
        "    options={\n",
        "        \"preserve_norm\": False,\n",
        "        \"trunc_params\": {\n",
        "            \"chi_max\": 64,\n",
        "            \"svd_min\": 1e-8,\n",
        "            \"trunc_cut\": None,\n",
        "        },\n",
        "        \"max_delta_t\": 2,\n",
        "    },\n",
        ")"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "2486594c",
      "metadata": {},
      "source": [
        "마지막으로, 초기 MPO 상태를 산출하는 를 `identity_factory` 정의하고, 층상 트로터 모델에서 사용하는 격자와 일치하는 MPS로서 네엘 초기 상태를 준비한다.\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 19,
      "id": "1e216575",
      "metadata": {},
      "outputs": [],
      "source": [
        "def identity_factory():\n",
        "    return MPOState.initialize_from_lattice(models[0].lat, conserve=True)\n",
        "\n",
        "\n",
        "mps_initial_state = MPS_neel_state(models[0].lat)"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "5658315c",
      "metadata": {},
      "source": [
        "공장이 설정되었으므로, 이제 각 진화 시점에서 동적 계수를 계산합니다. 각 $t$ 에 대해, `setup_dynamic_lse` TeNPy, 를 통해 관련 중첩 행렬을 구축하고, `setup_frobenius_problem` 프로베니우스 노름 비용을 최소화하는 를 `cvxpy.Problem` 반환합니다. `mpf_dynamic_coeffs_list`해법기는 해당 시간에 맞춰 조정된 계수 $x_j(t)$ 를 반환하며, 우리는 이를 에 수집합니다. 주어진 $t$ 에 대해 솔버가 실패할 경우, 계수를 0으로 설정하여 루프가 계속 진행되도록 합니다.\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 20,
      "id": "b05dc012",
      "metadata": {},
      "outputs": [
        {
          "name": "stdout",
          "output_type": "stream",
          "text": [
            "Computing dynamic coefficients for time=0.5\n",
            "\n",
            "Computing dynamic coefficients for time=0.6\n",
            "\n",
            "Computing dynamic coefficients for time=0.7\n",
            "\n",
            "Computing dynamic coefficients for time=0.7999999999999999\n",
            "\n",
            "Computing dynamic coefficients for time=0.8999999999999999\n",
            "\n",
            "Computing dynamic coefficients for time=0.9999999999999999\n",
            "\n",
            "Computing dynamic coefficients for time=1.0999999999999999\n",
            "\n",
            "Computing dynamic coefficients for time=1.1999999999999997\n",
            "\n",
            "Computing dynamic coefficients for time=1.2999999999999998\n",
            "\n",
            "Computing dynamic coefficients for time=1.4\n",
            "\n",
            "Computing dynamic coefficients for time=1.4999999999999998\n",
            "\n"
          ]
        }
      ],
      "source": [
        "mpf_dynamic_coeffs_list = []\n",
        "for t in trotter_times:\n",
        "    print(f\"Computing dynamic coefficients for time={t}\")\n",
        "    lse = setup_dynamic_lse(\n",
        "        mpf_trotter_steps,\n",
        "        t,\n",
        "        identity_factory,\n",
        "        exact_factory,\n",
        "        approx_factory,\n",
        "        mps_initial_state,\n",
        "    )\n",
        "    problem, coeffs = setup_frobenius_problem(lse)\n",
        "    try:\n",
        "        problem.solve()\n",
        "        mpf_dynamic_coeffs_list.append(coeffs.value)\n",
        "    except Exception as error:\n",
        "        mpf_dynamic_coeffs_list.append(np.zeros(len(mpf_trotter_steps)))\n",
        "        print(error, \"Calculation Failed for time\", t)\n",
        "    print(\"\")"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "4f8814e3",
      "metadata": {},
      "source": [
        "<span id=\"combine-trotter-expectation-values-with-the-mpf-coefficients\" />\n",
        "\n",
        "#### 트로터의 기대값과 MPF 계수를 결합한다\n",
        "\n",
        "이제 각 계수 집합(정적-정확, 정적-근사, 동적)에 대해 $\\langle A \\rangle_{\\text{MPF}}(t) = \\sum_j x_j \\, \\langle A \\rangle_{k_j}(t)$ 를 계산하고, 회로별 표준 오차를 전파한 뒤, 그 결과로 얻은 시계열을 정확한 대각화 곡선과 비교하여 그래프로 표시합니다.\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 21,
      "id": "35042576",
      "metadata": {},
      "outputs": [
        {
          "data": {
            "text/plain": [
              "<Image src=\"/docs/images/tutorials/multi-product-formula/extracted-outputs/35042576-0.avif\" alt=\"Output of the previous code cell\" />"
            ]
          },
          "metadata": {},
          "output_type": "display_data"
        }
      ],
      "source": [
        "sym = {1: \"^\", 2: \"s\", 4: \"p\"}\n",
        "# Get expectation values at all times for each Trotter step\n",
        "for k, step in enumerate(mpf_trotter_steps):\n",
        "    trotter_curve, trotter_curve_error = [], []\n",
        "    for trotter_expvals, trotter_stds in zip(\n",
        "        mpf_expvals_all_times, mpf_stds_all_times\n",
        "    ):\n",
        "        trotter_curve.append(trotter_expvals[k])\n",
        "        trotter_curve_error.append(trotter_stds[k])\n",
        "\n",
        "    plt.errorbar(\n",
        "        trotter_times,\n",
        "        trotter_curve,\n",
        "        yerr=trotter_curve_error,\n",
        "        alpha=0.5,\n",
        "        markersize=4,\n",
        "        marker=sym[step],\n",
        "        color=\"grey\",\n",
        "        label=f\"{mpf_trotter_steps[k]} Trotter steps\",\n",
        "    )\n",
        "\n",
        "# Get expectation values at all times for the static MPF with exact coeffs\n",
        "exact_mpf_curve, exact_mpf_curve_error = [], []\n",
        "for trotter_expvals, trotter_stds in zip(\n",
        "    mpf_expvals_all_times, mpf_stds_all_times\n",
        "):\n",
        "    mpf_std = np.sqrt(\n",
        "        sum(\n",
        "            [\n",
        "                (coeff**2) * (std**2)\n",
        "                for coeff, std in zip(coeffs_exact.value, trotter_stds)\n",
        "            ]\n",
        "        )\n",
        "    )\n",
        "    exact_mpf_curve_error.append(mpf_std)\n",
        "    exact_mpf_curve.append(trotter_expvals @ coeffs_exact.value)\n",
        "\n",
        "plt.errorbar(\n",
        "    trotter_times,\n",
        "    exact_mpf_curve,\n",
        "    yerr=exact_mpf_curve_error,\n",
        "    markersize=4,\n",
        "    marker=\"o\",\n",
        "    label=\"Static MPF - Exact\",\n",
        "    color=\"purple\",\n",
        ")\n",
        "\n",
        "\n",
        "# Get expectation values at all times for the static MPF with approximate coeffs\n",
        "approx_mpf_curve, approx_mpf_curve_error = [], []\n",
        "for trotter_expvals, trotter_stds in zip(\n",
        "    mpf_expvals_all_times, mpf_stds_all_times\n",
        "):\n",
        "    mpf_std = np.sqrt(\n",
        "        sum(\n",
        "            [\n",
        "                (coeff**2) * (std**2)\n",
        "                for coeff, std in zip(coeffs_approx.value, trotter_stds)\n",
        "            ]\n",
        "        )\n",
        "    )\n",
        "    approx_mpf_curve_error.append(mpf_std)\n",
        "    approx_mpf_curve.append(trotter_expvals @ coeffs_approx.value)\n",
        "\n",
        "plt.errorbar(\n",
        "    trotter_times,\n",
        "    approx_mpf_curve,\n",
        "    yerr=approx_mpf_curve_error,\n",
        "    markersize=4,\n",
        "    marker=\"o\",\n",
        "    label=\"Static MPF - Approx\",\n",
        "    color=\"orange\",\n",
        ")\n",
        "\n",
        "\n",
        "# Get expectation values at all times for the dynamic MPF\n",
        "dynamic_mpf_curve, dynamic_mpf_curve_error = [], []\n",
        "for trotter_expvals, trotter_stds, dynamic_coeffs in zip(\n",
        "    mpf_expvals_all_times, mpf_stds_all_times, mpf_dynamic_coeffs_list\n",
        "):\n",
        "    mpf_std = np.sqrt(\n",
        "        sum(\n",
        "            [\n",
        "                (coeff**2) * (std**2)\n",
        "                for coeff, std in zip(dynamic_coeffs, trotter_stds)\n",
        "            ]\n",
        "        )\n",
        "    )\n",
        "    dynamic_mpf_curve_error.append(mpf_std)\n",
        "    dynamic_mpf_curve.append(trotter_expvals @ dynamic_coeffs)\n",
        "\n",
        "plt.errorbar(\n",
        "    trotter_times,\n",
        "    dynamic_mpf_curve,\n",
        "    yerr=dynamic_mpf_curve_error,\n",
        "    markersize=4,\n",
        "    marker=\"o\",\n",
        "    label=\"Dynamic MPF\",\n",
        "    color=\"pink\",\n",
        ")\n",
        "\n",
        "\n",
        "# Exact expectation values\n",
        "plt.plot(\n",
        "    exact_evolution_times,\n",
        "    exact_expvals,\n",
        "    color=\"red\",\n",
        "    linestyle=\"--\",\n",
        "    label=\"Exact time-evolution\",\n",
        ")\n",
        "\n",
        "plt.title(f\"$\\\\langle Z_{{{L//2-1}}} Z_{{{L//2}}} \\\\rangle$ vs time\")\n",
        "plt.xlabel(\"Time\")\n",
        "plt.ylabel(\"Expectation Value\")\n",
        "plt.legend(loc=\"upper center\", bbox_to_anchor=(0.5, -0.2), ncol=2)\n",
        "plt.grid(alpha=0.1)\n",
        "plt.tight_layout()\n",
        "plt.show()"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "34923748",
      "metadata": {},
      "source": [
        "위의 그래프는 트로터 오차와 표본 오차 간의 상호작용을 보여줍니다.\n",
        "\n",
        "* **트로터 오류.** 시간이 지남에 따라 개별 제품의 공식(회색 마커)은 정확한 곡선에서 점점 더 멀어집니다. $k=1$ 회로는 편차가 가장 크고 가장 얕지만, 이미 $t/k \\gtrsim 1$ 인 영역에 속하므로, 주요 오차 항인 $1/k^{2}$ 의 값이 큽니다. MPF 조합(색상 표시기)은 이러한 선행 트로터 오차 항 중 몇 가지를 상쇄하므로, 어떤 단일 $k_j$ 회로보다 훨씬 더 정확하게 곡선을 따라갑니다. 남아 있는 오차는 MPF가 상쇄 *하지* 못하는 고차 트로터 항을 반영합니다. 즉, $2$, $r=3$ 정적 MPF는 오차의 첫 두 차수만 제거할 뿐이며, $t/k_{\\min}$ 값이 커지면 상쇄되지 않은 잔여 오차가 결국 지배적이게 됩니다. 따라서 MPF는 매우 얕은 회로가 임의의 시점에서 정확성을 유지한다고 보장하지 않습니다.\n",
        "\n",
        "* **표본 오차.** MPF 곡선에서 오차 막대가 더 넓게 나타나는 것은 선형 조합의 직접적인 결과입니다. 회로별 독립 표준 오차 $\\sigma_{k_j}$ 를 합산하면 총 분산 $\\sigma_{\\text{MPF}}^2 = \\sum_j x_j^2 \\, \\sigma_{k_j}^2$ 이 됩니다. 따라서 $\\|x\\|_2$ 가 클수록(실제로는 우리가 제어하는 $\\|x\\|_1$ 가 클수록), 주어진 목표 불확실도에 도달하기 위해 더 많은 측정 횟수가 필요합니다. 이것이 ‘Background’의 근사 해법기 옵션에 숨겨진 절충점입니다. 즉, 이 오버헤드를 관리 가능한 수준으로 유지하기 위해 $\\|x\\|_1$ 에 상한을 설정합니다. 중요한 점은, 트로터 오차와 달리 표본 오차는 $1/\\sqrt{N_{\\text{shots}}}$ 에 비례하여 줄어들기 때문에, 더 많은 샷을 사용함으로써 항상 이를 줄일 수 있다는 것이다.\n",
        "\n",
        "아래의 대규모 하드웨어 예시에서, 하드웨어 노이즈는 각 $\\langle A \\rangle_{k_j}$ 에 추가적인 오차 원인으로 유입되며, 이 역시 MPF 계수에 의해 증폭됩니다. 해당 섹션에서는 오류 완화 기법이 MPF와 어떻게 상호작용하는지 살펴보겠습니다.\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "6fa763ff",
      "metadata": {},
      "source": [
        "<span id=\"large-scale-hardware-example\" />\n",
        "\n",
        "## 대규모 하드웨어 예시\n",
        "\n",
        "이 절에서는 문제를 시뮬레이션으로 정확히 재현할 수 있는 범위를 넘어 확장해 보겠습니다. 우리는 시간 $t = 3$ 에서 50-큐비트 XXZ 사슬을 사용하여 문헌 [\\[3\\]](#references) 에 제시된 결과 중 일부를 재현합니다. 소규모 예제와 동일한 4단계 워크플로를 따르되, 이번에는 오류 완화 기능이 적용된 실제 양자 하드웨어를 대상으로 합니다. 템플릿에서와 마찬가지로, 각 단계는 코드 내에 직접 표시되어 있으며, 중간 결과를 살펴볼 필요가 있는 경우 하나의 단계가 여러 셀에 걸쳐 표시될 수 있습니다.\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "481fea70",
      "metadata": {},
      "source": [
        "이 매핑 과정은 소규모 예시와 유사합니다. 즉, 해밀토니안을 정의하고, 트로터 매개변수를 선택하며, MPF 계수(정적 및 동적)를 계산한 뒤, 회로를 구성합니다. 주요 차이점은 다음과 같습니다:\n",
        "\n",
        "* $\\mathcal{U}(0.5, 1.5)$ (참고문헌 [\\[3\\]](#references) )에서 추출한 무작위 결합을 갖는 50개 사이트의 **XXZ 해밀토니안**.\n",
        "* $k_j = [3, 4, 6]$ 인 **대칭** 2차 트로터 공식 (따라서 $\\chi=1$, `symmetric=True`).\n",
        "* 단일 고정 진화 시간 $t = 3$. $k_{\\min}=3$ 를 대입하면 $t/k_{\\min}=1$ 가 되며, 이를 통해 얕은 구성 요소들이 MPF가 의존하는 선행 오차 모델이 유효한 트로터 수렴 영역 내에 유지된다.\n",
        "* 기준으로 삼기 위해, **‘ $k = 10$** ’ 트로터 단계를 사용한 단일 회로 비교 실험을 추가로 수행하였다. 우리가 $k = 10$ 를 선택한 이유는, 이 회로의 하드웨어 상 2-큐비트 처리 깊이가 가장 깊은 MPF 구성 요소( $k_{\\max}=6$ )에 다중 MPF 회로 실행에 따른 오버헤드를 더한 값보다 더 깊기 때문입니다. 이 깊이는 노이즈 제한 영역에 해당할 만큼 충분히 깊으며, 이 영역에서 MPF 조합이 단일 회로 기준선보다 우수한 성능을 보일 것으로 예상됩니다. 이는 MPF 조합에 대한 “단일 심층 회로” 비교일 뿐, MPF의 유효 트로터 오차를 대상으로 하는 회로는 아닙니다(후자의 경우 훨씬 더 많은 단계가 필요하기 때문입니다).\n",
        "\n",
        "여기서는 아직 1단계(매핑 및 회로 구성)에 있지만, 이 셀에서는 정적 계수뿐만 아니라 동적 계수도 미리 계산한다는 점에 유의하십시오. 동적 계수는 $H$ 및 $t$ 에 따라 달라지지만 양자 측정 결과에는 영향을 받지 않으므로, 4단계 이전의 어느 시점에서나 계산할 수 있습니다. MPF와 관련된 모든 설정을 한곳에 모아두기 위해 지금 이렇게 하고 있습니다.\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 22,
      "id": "a019ac32",
      "metadata": {},
      "outputs": [
        {
          "name": "stdout",
          "output_type": "stream",
          "text": [
            "Static coefficients: [ 0.42857143 -1.82857143  2.4       ]\n",
            "L1 norm: 4.65714285714286\n",
            "Approximate coefficients: [-0.4942491   0.40206845  1.09218065]\n",
            "L1 norm (approx): 1.9884981979026675\n",
            "Computing dynamic coefficients for time=3\n"
          ]
        }
      ],
      "source": [
        "# -------------------------Step 1-------------------------\n",
        "L = 50\n",
        "coupling_map = CouplingMap.from_line(L, bidirectional=False)\n",
        "\n",
        "# XXZ Hamiltonian with random couplings (Ref. [3])\n",
        "np.random.seed(0)\n",
        "even_edges = list(coupling_map.get_edges())[::2]\n",
        "odd_edges = list(coupling_map.get_edges())[1::2]\n",
        "\n",
        "Js = np.random.uniform(0.5, 1.5, size=L)\n",
        "hamiltonian = SparsePauliOp(Pauli(\"I\" * L))\n",
        "for i, edge in enumerate(even_edges + odd_edges):\n",
        "    hamiltonian += SparsePauliOp.from_sparse_list(\n",
        "        [\n",
        "            (\"XX\", (edge), 2 * Js[i]),\n",
        "            (\"YY\", (edge), 2 * Js[i]),\n",
        "            (\"ZZ\", (edge), 4 * Js[i]),\n",
        "        ],\n",
        "        num_qubits=L,\n",
        "    )\n",
        "\n",
        "observable = SparsePauliOp.from_sparse_list(\n",
        "    [(\"ZZ\", (L // 2 - 1, L // 2), 1.0)], num_qubits=L\n",
        ")\n",
        "\n",
        "total_time = 3\n",
        "mpf_trotter_steps = [3, 4, 6]\n",
        "order = 2\n",
        "symmetric = True\n",
        "\n",
        "# Static coefficients\n",
        "lse = setup_static_lse(mpf_trotter_steps, order=order, symmetric=symmetric)\n",
        "mpf_coeffs = lse.solve()\n",
        "print(f\"Static coefficients: {mpf_coeffs}\")\n",
        "print(f\"L1 norm: {np.linalg.norm(mpf_coeffs, ord=1)}\")\n",
        "\n",
        "model_approx, coeffs_approx = setup_sum_of_squares_problem(\n",
        "    lse, max_l1_norm=2.0\n",
        ")\n",
        "model_approx.solve()\n",
        "print(f\"Approximate coefficients: {coeffs_approx.value}\")\n",
        "print(f\"L1 norm (approx): {np.linalg.norm(coeffs_approx.value, ord=1)}\")\n",
        "\n",
        "# -------------------------Dynamic coefficients-------------------------\n",
        "single_2nd_order_circ = generate_time_evolution_circuit(\n",
        "    hamiltonian, time=1.0, synthesis=SuzukiTrotter(reps=1, order=order)\n",
        ")\n",
        "single_2nd_order_circ = pm.run(single_2nd_order_circ)\n",
        "\n",
        "layers = slice_by_depth(single_2nd_order_circ, max_slice_depth=1)\n",
        "models = [\n",
        "    LayerModel.from_quantum_circuit(layer, conserve=\"Sz\") for layer in layers\n",
        "]\n",
        "\n",
        "approx_factory = partial(\n",
        "    LayerwiseEvolver,\n",
        "    layers=models,\n",
        "    options={\n",
        "        \"preserve_norm\": False,\n",
        "        \"trunc_params\": {\"chi_max\": 64, \"svd_min\": 1e-8, \"trunc_cut\": None},\n",
        "        \"max_delta_t\": 4,\n",
        "    },\n",
        ")\n",
        "\n",
        "single_4th_order_circ = generate_time_evolution_circuit(\n",
        "    hamiltonian, time=1.0, synthesis=SuzukiTrotter(reps=1, order=4)\n",
        ")\n",
        "single_4th_order_circ = pm.run(single_4th_order_circ)\n",
        "exact_model_layers = [\n",
        "    LayerModel.from_quantum_circuit(layer, conserve=\"Sz\")\n",
        "    for layer in slice_by_depth(single_4th_order_circ, max_slice_depth=1)\n",
        "]\n",
        "\n",
        "exact_factory = partial(\n",
        "    LayerwiseEvolver,\n",
        "    layers=exact_model_layers,\n",
        "    dt=0.1,\n",
        "    options={\n",
        "        \"preserve_norm\": False,\n",
        "        \"trunc_params\": {\"chi_max\": 64, \"svd_min\": 1e-8, \"trunc_cut\": None},\n",
        "        \"max_delta_t\": 3,\n",
        "    },\n",
        ")\n",
        "\n",
        "\n",
        "def identity_factory():\n",
        "    return MPOState.initialize_from_lattice(models[0].lat, conserve=True)\n",
        "\n",
        "\n",
        "mps_initial_state = MPS_neel_state(models[0].lat)\n",
        "\n",
        "print(f\"Computing dynamic coefficients for time={total_time}\")\n",
        "lse_dyn = setup_dynamic_lse(\n",
        "    mpf_trotter_steps,\n",
        "    total_time,\n",
        "    identity_factory,\n",
        "    exact_factory,\n",
        "    approx_factory,\n",
        "    mps_initial_state,\n",
        ")\n",
        "problem, coeffs_dyn = setup_frobenius_problem(lse_dyn)\n",
        "try:\n",
        "    problem.solve()\n",
        "    mpf_dynamic_coeffs = coeffs_dyn.value\n",
        "except Exception as error:\n",
        "    mpf_dynamic_coeffs = np.zeros(len(mpf_trotter_steps))\n",
        "    print(error, \"Calculation Failed\")\n",
        "\n",
        "# -------------------------Step 1 (cont): Build circuits-------------------------\n",
        "mpf_circuits = []\n",
        "for k in mpf_trotter_steps:\n",
        "    circuit = QuantumCircuit(L)\n",
        "    circuit.x([i for i in range(L) if i % 2])\n",
        "    trotter_circ = generate_time_evolution_circuit(\n",
        "        hamiltonian,\n",
        "        synthesis=SuzukiTrotter(reps=k, order=order),\n",
        "        time=total_time,\n",
        "    )\n",
        "    circuit.compose(trotter_circ, qubits=range(L), inplace=True)\n",
        "    mpf_circuits.append(circuit)\n",
        "\n",
        "# Baseline \"single deep circuit\" comparison run with k=10 Trotter steps.\n",
        "# Its two-qubit depth is deeper than the deepest MPF constituent (k_max=6) plus\n",
        "# the overhead of running multiple circuits, pushing it into the noise-limited\n",
        "# regime where MPF is expected to outperform. It does NOT target the MPF's effective\n",
        "# Trotter error (which would require many more steps).\n",
        "comp_circuit = QuantumCircuit(L)\n",
        "comp_circuit.x([i for i in range(L) if i % 2])\n",
        "trotter_circ = generate_time_evolution_circuit(\n",
        "    hamiltonian,\n",
        "    synthesis=SuzukiTrotter(reps=10, order=order),\n",
        "    time=total_time,\n",
        ")\n",
        "comp_circuit.compose(trotter_circ, qubits=range(L), inplace=True)\n",
        "mpf_circuits.append(comp_circuit)"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "b5d388b1",
      "metadata": {},
      "source": [
        "이제 선택한 백엔드에 맞춰 회로를 최적화합니다. `optimization_level=3`우리는 Qiskit의 사전 설정 패스 매니저를 사용하며, 이 매니저는 적절한 물리적 큐비트 세트를 자동으로 선택하고 각 회로를 장치 토폴로지에 매핑합니다.\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 23,
      "id": "05bad997",
      "metadata": {},
      "outputs": [
        {
          "name": "stdout",
          "output_type": "stream",
          "text": [
            "<IBMBackend('ibm_fez')>\n"
          ]
        }
      ],
      "source": [
        "# -------------------------Step 2-------------------------\n",
        "service = QiskitRuntimeService()\n",
        "# backend = service.least_busy(operational=True, simulator=False, min_num_qubits=L)\n",
        "backend = service.backend(\"ibm_fez\")\n",
        "print(backend)\n",
        "\n",
        "transpiler = generate_preset_pass_manager(\n",
        "    optimization_level=3, backend=backend\n",
        ")\n",
        "transpiled_circuits = [transpiler.run(circ) for circ in mpf_circuits]\n",
        "\n",
        "isa_observables = [\n",
        "    observable.apply_layout(circ.layout) for circ in transpiled_circuits\n",
        "]"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "b578c285",
      "metadata": {},
      "source": [
        "실제 하드웨어에서 더 복잡한 회로를 구동하려면 강력한 오류 완화 조치가 필요합니다. 본 연구에서는 동적 분리, 게이트 및 측정 트위링, 측정 오차 완화, 제로 노이즈 외삽법(ZNE)을 구현합니다. 여기서 사용하는 ZNE 잡음 계수(`1, 1.2, 1.4`)는 얕은 회로 시나리오에서보다 작다는 점에 유의해야 합니다. 이는 더 깊은 MPF 구성 요소들이 이미 잡음 임계치에 근접해 있으며, 잡음이 크게 증폭될 경우 ZNE 외삽이 신뢰할 수 있는 범위를 벗어나게 되기 때문입니다.\n",
        "\n",
        "우리는 4개의 회로 전체( $k_j = [3, 4, 6]$ 의 MPF 구성 요소 3개와 $k = 10$ 의 기준선)를 하나의 Estimator 작업으로 제출합니다.\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 24,
      "id": "e2722b61",
      "metadata": {},
      "outputs": [],
      "source": [
        "# -------------------------Step 3-------------------------\n",
        "estimator = Estimator(mode=backend)\n",
        "estimator.options.default_shots = 30000\n",
        "\n",
        "# Error suppression/mitigation\n",
        "estimator.options.dynamical_decoupling.enable = True\n",
        "estimator.options.twirling.enable_gates = True\n",
        "estimator.options.twirling.enable_measure = True\n",
        "estimator.options.twirling.num_randomizations = \"auto\"\n",
        "estimator.options.twirling.strategy = \"active-accum\"\n",
        "estimator.options.resilience.measure_mitigation = True\n",
        "estimator.options.experimental.execution_path = \"gen3-turbo\"\n",
        "\n",
        "estimator.options.resilience.zne_mitigation = True\n",
        "estimator.options.resilience.zne.noise_factors = (1, 1.2, 1.4)\n",
        "estimator.options.resilience.zne.extrapolator = \"linear\"\n",
        "\n",
        "estimator.options.environment.job_tags = [\"TUT_MPF\"]\n",
        "\n",
        "job_50 = estimator.run(\n",
        "    [\n",
        "        (circ, observable)\n",
        "        for circ, observable in zip(transpiled_circuits, isa_observables)\n",
        "    ]\n",
        ")"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "afc0c029",
      "metadata": {},
      "source": [
        "작업 결과에서 회로별 기대값과 표준편차를 추출한 다음, 소규모 예제와 정확히 동일한 방식으로 각 MPF 계수 집합과 결합합니다: $\\langle A \\rangle_{\\text{MPF}} = \\sum_j x_j \\, \\langle A \\rangle_{k_j}$, 전파된 분산은 $\\sigma^2 = \\sum_j x_j^2 \\sigma_{k_j}^2$ 입니다.\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 25,
      "id": "a924d79c",
      "metadata": {},
      "outputs": [
        {
          "name": "stdout",
          "output_type": "stream",
          "text": [
            "[array(-0.07916195), array(-0.04479681), array(-0.2560756), array(-0.06045848)]\n",
            "[array(0.04605538), array(0.10056336), array(0.14426151), array(0.04059092)]\n"
          ]
        }
      ],
      "source": [
        "# -------------------------Step 4-------------------------\n",
        "result = job_50.result()\n",
        "evs = [res.data.evs for res in result]\n",
        "std = [res.data.stds for res in result]\n",
        "\n",
        "print(evs)\n",
        "print(std)"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 26,
      "id": "1071de0d",
      "metadata": {},
      "outputs": [
        {
          "name": "stdout",
          "output_type": "stream",
          "text": [
            "Exact static MPF expectation value:  -0.5665938395816946 +- 0.3925273058119915\n",
            "Approximate static MPF expectation value:  -0.25856647611537903 +- 0.164249927266166\n",
            "Dynamic MPF expectation value:  -0.12667812062949296 +- 0.06059471006973169\n"
          ]
        }
      ],
      "source": [
        "exact_mpf_std = np.sqrt(\n",
        "    sum([(coeff**2) * (std**2) for coeff, std in zip(mpf_coeffs, std[:3])])\n",
        ")\n",
        "print(\n",
        "    \"Exact static MPF expectation value: \",\n",
        "    evs[:3] @ mpf_coeffs,\n",
        "    \"+-\",\n",
        "    exact_mpf_std,\n",
        ")\n",
        "approx_mpf_std = np.sqrt(\n",
        "    sum(\n",
        "        [\n",
        "            (coeff**2) * (std**2)\n",
        "            for coeff, std in zip(coeffs_approx.value, std[:3])\n",
        "        ]\n",
        "    )\n",
        ")\n",
        "print(\n",
        "    \"Approximate static MPF expectation value: \",\n",
        "    evs[:3] @ coeffs_approx.value,\n",
        "    \"+-\",\n",
        "    approx_mpf_std,\n",
        ")\n",
        "dynamic_mpf_std = np.sqrt(\n",
        "    sum(\n",
        "        [\n",
        "            (coeff**2) * (std**2)\n",
        "            for coeff, std in zip(mpf_dynamic_coeffs, std[:3])\n",
        "        ]\n",
        "    )\n",
        ")\n",
        "print(\n",
        "    \"Dynamic MPF expectation value: \",\n",
        "    evs[:3] @ mpf_dynamic_coeffs,\n",
        "    \"+-\",\n",
        "    dynamic_mpf_std,\n",
        ")"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 27,
      "id": "64360d85",
      "metadata": {},
      "outputs": [
        {
          "data": {
            "text/plain": [
              "<Image src=\"/docs/images/tutorials/multi-product-formula/extracted-outputs/64360d85-0.avif\" alt=\"Output of the previous code cell\" />"
            ]
          },
          "metadata": {},
          "output_type": "display_data"
        }
      ],
      "source": [
        "sym = {3: \"^\", 4: \"s\", 6: \"p\"}\n",
        "for k, step in enumerate(mpf_trotter_steps):\n",
        "    plt.errorbar(\n",
        "        k,\n",
        "        evs[k],\n",
        "        yerr=std[k],\n",
        "        alpha=0.5,\n",
        "        markersize=4,\n",
        "        marker=sym[step],\n",
        "        color=\"grey\",\n",
        "        label=f\"{mpf_trotter_steps[k]} Trotter steps\",\n",
        "    )\n",
        "\n",
        "plt.errorbar(\n",
        "    3,\n",
        "    evs[-1],\n",
        "    yerr=std[-1],\n",
        "    alpha=0.5,\n",
        "    markersize=8,\n",
        "    marker=\"x\",\n",
        "    color=\"blue\",\n",
        "    label=\"10 Trotter steps\",\n",
        ")\n",
        "\n",
        "plt.errorbar(\n",
        "    4,\n",
        "    evs[:3] @ mpf_coeffs,\n",
        "    yerr=exact_mpf_std,\n",
        "    markersize=4,\n",
        "    marker=\"o\",\n",
        "    color=\"purple\",\n",
        "    label=\"Static MPF\",\n",
        ")\n",
        "\n",
        "plt.errorbar(\n",
        "    5,\n",
        "    evs[:3] @ coeffs_approx.value,\n",
        "    yerr=approx_mpf_std,\n",
        "    markersize=4,\n",
        "    marker=\"o\",\n",
        "    color=\"orange\",\n",
        "    label=\"Approximate static MPF\",\n",
        ")\n",
        "\n",
        "plt.errorbar(\n",
        "    6,\n",
        "    evs[:3] @ mpf_dynamic_coeffs,\n",
        "    yerr=dynamic_mpf_std,\n",
        "    markersize=4,\n",
        "    marker=\"o\",\n",
        "    color=\"pink\",\n",
        "    label=\"Dynamic MPF\",\n",
        ")\n",
        "\n",
        "exact_obs = -0.24384471447172074  # Calculated via Tensor Network calculation\n",
        "plt.axhline(\n",
        "    y=exact_obs, linestyle=\"--\", color=\"red\", label=\"Exact time-evolution\"\n",
        ")\n",
        "\n",
        "plt.title(\n",
        "    f\"$\\\\langle Z_{{{L//2-1}}} Z_{{{L//2}}} \\\\rangle$ at time {total_time} for the different methods\"\n",
        ")\n",
        "plt.xlabel(\"Method\")\n",
        "plt.ylabel(\"Expectation Value\")\n",
        "plt.legend(loc=\"upper center\", bbox_to_anchor=(0.5, -0.2), ncol=2)\n",
        "plt.grid(alpha=0.1)\n",
        "plt.tight_layout()\n",
        "plt.show()"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "31effe5f",
      "metadata": {},
      "source": [
        "위의 하드웨어 결과에 대해 몇 가지 관찰 사항을 말씀드리자면:\n",
        "\n",
        "* **하드웨어 측면에서 더 깊이 파고드는 데는 비용이 듭니다.** 단일 회로 기준선은 상황을 명확히 보여줍니다. $k = 6$ 회로는 사실상 정확합니다( $-0.256$ 대 기준값 $-0.244$ ). 반면, 더 깊은 $k = 10$ 기준선은 *더* 나쁘며( $-0.061$, $\\sim 0.18$ 의 오차), 더 나은 것은 아닙니다. 트로터 오차가 이미 작을 경우, 단계를 추가하면 주로 회로가 더 깊어지면서 게이트 노이즈와 비결합 현상이 더 많이 누적될 뿐이다. 이것이 바로 MPF가 설계된 목적입니다. 즉, 얕은 구성 요소만을 사용하여 깊은 회로 수준의 정확도를 달성하는 것입니다.\n",
        "\n",
        "* **소노름 MPF가 딥 싱글 서킷보다 성능이 더 우수하다.** 대략적인 정적 MPF( $\\|x\\|_1 \\approx 2$ 로 상한 설정)는 $-0.259$ 에 도달하며, 이는 기준치보다 $\\sim 0.015$ 정도 낮은 수치로, $k = 10$ 의 기준치보다는 훨씬 더 가까운 수준입니다. 동적 MPF( $-0.127$ ) 역시 그 기준치를 여유 있게 상회합니다. 두 방법 모두 얕은 $k_j = [3, 4, 6]$ 회로만을 결합하지만, 깊은 단일 회로로는 도출할 수 없었던 답을 찾아냅니다.\n",
        "\n",
        "* **계수 노름은 수학적 최적성보다 더 중요합니다.** 정확한 정적 MPF는 $\\|x\\|_1 = 4.66$ 를 가지며, 모든 추정기 중 *가장* 성능이 떨어집니다( $-0.567$, 오차 범위가 $0.3$ 보다 큽니다). 계수의 노름이 크기 때문에 각 $\\langle A \\rangle_{k_j}$ 에 대한 잔류 게이트 노이즈, 비고전성, ZNE 오차가 대략 동일한 배수로 증폭되어, 이를 통해 얻는 트로터 오차 상쇄 효과를 압도해 버립니다. 노름(대략적 정적 해법, $\\|x\\|_1 \\approx 2$ )에 상한을 설정하면 이러한 과부하 현상이 해소되고 최상의 추정값을 얻을 수 있다. 비록 그 계수들이 더 이상 주요 트로터 오차를 정확히 상쇄하지는 못하더라도 말이다.\n",
        "\n",
        "* **개별 얕은 코스도 여전히 경쟁력을 갖출 수 있다.** 이곳에서 유일한 $k = 6$ 구성 요소( $-0.256$ )는 그 자체로 본질적으로 정확한 값을 나타냅니다. 이번 실행에서는 근사 정적 MPF보다 심지어 아주 약간 더 가까운 값을 보여줍니다. 문제는 “수렴했으나 아직 잡음에 의해 제한되지는 않은” 최적의 지점에 *어떤* $k$ 가 위치하는지 미리 알 수 없다는 점이며, 트로터 수렴을 보장하기 위해 단순히 더 깊은 곳( $k = 10$ )으로 가는 것처럼 안전해 보이는 선택이 바로 실패하는 선택이라는 것입니다. MPF는 적절한 깊이를 추측할 필요가 없는, 얕은 회로들의 원리에 입각한 조합을 제공합니다.\n",
        "\n",
        "실무적으로 얻을 수 있는 교훈은, 하드웨어 환경에서 MPF를 사용할 때는 각 개별 $\\langle A \\rangle_{k_j}$ 에 대해 강력한 오차 완화 기법을 병행해야 하며, 계수 $L_1$ -노름은 적정 수준으로 유지해야 하고(근사 해법기 또는 동적 MPF 사용), 트로터 단계 $k_j$ 는 $t/k_{\\min} \\lesssim 1$ 가 성립하도록 선택해야 한다는 점입니다. 여기서 $t = 3$ 의 $k_{\\min} = 3$ 를 적용하면 $t/k_{\\min} = 1$ 가 되며, 이를 통해 정적 MPF가 의존하는 선행 오차 모델이 유효한 수렴 영역 내에 구성 요소들을 유지할 수 있습니다. 이러한 선택에 따라, 여기에서 소노름 MPF는 수렴된 단일 회로와 동등한 성능을 보이는 반면, 단순히 “깊이만 늘리는” 방식의 기준 모델은 그렇지 못하여, 문헌 [\\[3\\]](#references) 에서 제시된 ‘깊이 대 정확도’의 이점을 재현해 냅니다. 또한 개별 실행 결과에는 잡음이 존재한다는 점에 유의해야 합니다. 동일한 작업을 다른 방식으로 제출하거나(또는 다른 백엔드에서 실행할 경우) 정확한 순위가 달라질 수 있습니다. 그러나 확고한 경향은 다음과 같습니다. small- $\\|x\\|_1$ 의 MPF는 좋은 성능을 보이며, large- $\\|x\\|_1$ 의 exact-static MPF는 하드웨어 잡음의 영향을 더 크게 받으며, over-deep 단일 회로는 잡음에 의해 성능이 제한됩니다.\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "5ac2f8a8",
      "metadata": {},
      "source": [
        "<span id=\"next-steps\" />\n",
        "\n",
        "## 다음 단계\n",
        "\n",
        "<Admonition type=\"tip\" title=\"권장사항\">\n",
        "  이 글이 흥미로웠다면, 다음 자료도 참고해 보시기 바랍니다:\n",
        "\n",
        "  * [MPF용 트로터 단계 선택 방법](https://qiskit.github.io/qiskit-addon-mpf/how_tos/choose_trotter_steps.html) — 불안정성을 방지하기 위한 ‘ $k_j$ ’ 값 선정에 관한 실용적인 지침\n",
        "  * [근사 모델 사용 방법](https://qiskit.github.io/qiskit-addon-mpf/how_tos/using_approximate_model.html) — 근사 정적 MPF에 대한 $L_1$ -노름 제약 조건 및 솔버 옵션 조정\n",
        "  * [`qiskit-addon-mpf` API 참조](https://qiskit.github.io/qiskit-addon-mpf/) — 정적, 동적 및 백엔드 모듈에 대한 전체 문서\n",
        "</Admonition>\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "70be41e1",
      "metadata": {},
      "source": [
        "<span id=\"references\" />\n",
        "\n",
        "## 참조\n",
        "\n",
        "\\[1] 바스케스, A. C., Egger, D. J., Ochsner, D., & Woerner, S. 하드웨어 친화적인 해밀토니안 시뮬레이션을 위한 잘 조건화된 다중 산출식. [Quantum, 제7권, 1067쪽 (2023)](https://quantum-journal.org/papers/q-2023-07-25-1067/)\n",
        "\n",
        "\\[2] Zhuk, S., Robertson, N. F., & Bravyi, S. 해밀토니안 시뮬레이션을 위한 트로터 오차 상한 및 동적 다중 곱 공식. [Physical Review Research, 6(3), 033309 (2024)](https://journals.aps.org/prresearch/abstract/10.1103/PhysRevResearch.6.033309)\n",
        "\n",
        "\\[3] 로버트슨, N. F., 등 텐서 네트워크를 활용한 동적 다중곱 공식. [arXiv:2407.17405 (2024)](https://arxiv.org/abs/2407.17405)\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {},
      "id": "a1b8767d",
      "source": "© IBM Corp., 2017-2026"
    }
  ],
  "metadata": {
    "kernelspec": {
      "display_name": "Python 3",
      "language": "python",
      "name": "python3"
    },
    "language_info": {
      "codemirror_mode": {
        "name": "ipython",
        "version": 3
      },
      "file_extension": ".py",
      "mimetype": "text/x-python",
      "name": "python",
      "nbconvert_exporter": "python",
      "pygments_lexer": "ipython3",
      "version": "3"
    },
    "hours": 1.5,
    "qpuSeconds": 240
  },
  "nbformat": 4,
  "nbformat_minor": 5
}