AQC + トロッター・ハミルトニアン動力学用の Qiskit Function テンプレートをデプロイして実行する
概要
これは、ハミルトニアン動力学のための、実験手法に依存しないQiskit関数テンプレートです。 1D の最近傍パウリ・ハミルトニアン、準備された初期状態(任意)、および観測量の一式が与えられると、本システムはトロッター時間発展、近似量子コンパイル(AQC)回路圧縮、およびリスク軽減実行を実行し、各観測量の時系列を返す。 セットアップ(PRE)と分析(POST)を入れ替えると、同じ中核要素が別の実験を導き出すことになる:
PRE(設定) | FUNCTION(ここにデプロイ済み) | 投稿(あなたの分析) |
|---|---|---|
| 回路状態または積状態として状態を準備し、必要に応じてローカルキックを適用する | トロッター合成 → AQC圧縮 → statevector、 fake、 または での実行 runtime、 を返す | 中性子散乱、あるいは磁化、輸送現象、クエンチダイナミクスなど |
このテンプレートは、他のアプリケーションテンプレートとともに、 Qiskit Function templates リポジトリで公開されています。 このノートブックは、ご自身の Qiskit Serverless アカウントにデプロイします。 一度実行すれば、どのノートブックからでもこの関数を呼び出すことができます serverless.load("aqc-dynamics-function")。
具体的な科学的な例については、 「AQC + Trotterダイナミクスを用いた中性子散乱のシミュレーション (サーバーレスワークフロー)」を参照してください。このワークフローでは、この関数を呼び出して、 KCuFの動的構造因子を計算しています。一方、このノートブックでは、デプロイメントと入力契約について解説しています。
要件
作業を開始する前に、このノートブックのカーネル環境に以下のものが用意されていることを確認してください:
- Qiskit SDK v2.0 またはそれ以降 (
pip install qiskit)。 - Qiskitの IBM カタログクライアント(
pip install qiskit-ibm-catalog)は、 Qiskit Serverless 上でワークロードをデプロイおよび実行します。
この関数独自の科学パッケージ(qiskit-addon-aqc-tensor, cotengrust, qiskit-aer)は、ローカルにインストールする必要はありません。
テンプレートのソースファイルを入手する
この関数は、クラウド上で Qiskit Serverless によって実行される Python の小さなパッケージであるため、そのソースコードはデプロイ時にアップロードされるローカルファイルとして存在している必要があります。 このパッケージは、Qiskit Function templates リポジトリで公開されています。
ダウンロード source_files
ダウンロードファイルは1つのZIPファイルで、リポジトリ内のディレクトリのフルパスにちなんで名付けられています:
qiskit-community qiskit-function-templates main physics aqc_trotter source_files.zip
- このノートブックが保存されているディレクトリに、そのファイルを解凍してください。
- 抽出されたフォルダの名前を、その長い名前から「.」に変更してください
source_files。
これで、作業ディレクトリは次のようになります:
your-working-directory/
├── function-template-aqc-trotter.ipynb <- this notebook
└── source_files/ <- the renamed folder
├── __init__.py
├── program.py
└── source/
├── __init__.py
├── _serverless.py
├── app_function.py
├── aqc.py
├── build.py
├── execute.py
└── hamiltonian.py
名前は正確に一致させる必要があります source_files。なぜなら、それが「ステップ working_dir 3」のアップロード項目だからです。
program.py ゲートウェイが呼び出すエントリポイントです。 「」以下の内容はすべて実装に関する source/ もので、段階ごとに分けられています:ハミルトニアンおよびトロッター合成、AQC圧縮、そして実行です。 以下の例を実行するために、編集する必要は一切ありません。 ステップ3ではディレクトリ全体がアップロードされるため、ファイルを変更するたびにその手順を繰り返してください。
1. 認証
を使用して、APIキー(トークン)と CRN (インスタンス) QiskitServerless で に認証を行 qiskit-ibm-catalog ってください。これらは、 IBM Quantum® Platform のダッシュボードで確認できます。 これらの認証情報を使用すると、サーバーレスクライアントをローカルで起動して、選択した関数をアップロードしたり実行したりすることができます:
from qiskit_ibm_catalog import QiskitServerless
serverless = QiskitServerless(channel="ibm_quantum_platform", token="MY_TOKEN", instance="MY_CRN")必要に応じて、ローカル環境に認証情報を保存 save_account() することもできます(「 IBM Cloud® アカウントの設定 」ガイドを参照してください)。 なお、これにより、認証情報が以下のファイルと同じファイルに書き込まれることに注意してください QiskitRuntimeService.save_account():
QiskitServerless.save_account(channel="ibm_quantum_platform", token="MY_TOKEN", instance="MY_CRN")アカウントが保存されていれば、認証のためにトークンを指定する必要はありません:
from qiskit_ibm_catalog import QiskitServerless
# Authenticate to the remote cluster
# In this case, loading a saved account
serverless = QiskitServerless()
# REPLACE WITH YOUR OWN CREDENTIALS or SAVED ACCOUNT
# serverless = QiskitServerless(channel="ibm_quantum_platform", token="MY_TOKEN", instance="MY_CRN")2. 依存関係を宣言する
マネージドのベースサーバーレスイメージに加えて、関数が必要とするパッケージをパッケージ化します。
ゲートウェイは、パッケージ名に基づいて照合され、. によって許可されたバージョンに固定された名前のみを、その許可リスト (requirements-dynamic-dependencies.txt) に登録します ==。 それ以外のものはすべて、間接的に(許可リストに登録されたパッケージの依存関係として)導入される必要があります。 この構文 [extras] は有効です: qiskit-addon-aqc-tensor[quimb-jax] が と quimb をインストールします jax。 cotengrust テンソルネットワークのシミュレーションにおいて、メモリ効率を高めるために必要です。 qiskit-aer バック fake エンド(ローカルノイズ付きシミュレーション)については、別途記載されています。
DEPENDENCIES = [
"qiskit-addon-aqc-tensor[quimb-jax]==0.3.1",
"qiskit-aer==0.17.2",
"cotengrust==0.2.0",
]3. 関数を定義してアップロードする
from qiskit_ibm_catalog import QiskitFunction
fn = QiskitFunction(
title="aqc-dynamics-function",
entrypoint="program.py",
working_dir="source_files/",
dependencies=DEPENDENCIES,
)
serverless.upload(fn)Output:
QiskitFunction(aqc-dynamics-function)
4. 登録されていることを確認する
next(p for p in serverless.list() if p.title == "aqc-dynamics-function")Output:
QiskitFunction(aqc-dynamics-function)
関数
参照
これは簡単な紹介です。 各フィールドについては、「 AQC Dynamics テンプレートの README 」に詳細が記載されています。そこには、検証ルールを含む完全な入力テーブル、出力フィールド、実行バックエンド、およびその他の具体例が記載されています。 以下は要約版です。続く例文を読むには十分な内容となっています。
入力
1回の実行は1回の呼び出し fn.run(...) に相当します。 表のうち、最初の3つの入力( hamiltonian, t_steps, および )のみが必須です aqc_segments。 これら以降の項目はすべてオプションであり、指定しない場合は表示されているデフォルト値が適用されます。したがって、最小限の呼び出しでは引数を3つ指定すればよく、表の残りの部分はオプションで利用可能な機能となります。 ハミルトニアンが鎖の長さを決定 num_qubits するため、別途サイズを入力する必要はありません。
入力 | デフォルト | 説明 | コラム「 4 」 |
|---|---|---|---|
hamiltonian | 必須 | 1D. としての最近傍法によるパウリ・ハミルトニアン SparsePauliOp。 ストリングはパウリ演算子であるため、暗黙の「1/2」という係数は存在しない。 | |
t_steps | 必須 | トロッターの歩数合計。 …へと発展し T = t_steps * dt 、各 においてすべての観測量について報告する t_k = k * dt。 | |
aqc_segments | 必須 | 圧縮計画:以下のリスト {"n_steps": k, "ansatz_steps": m}。 sum(n_steps) ステップは圧縮されており、残りは通常のトロッターとして実行されます。 | |
dt | 0.2 | 物理時間がトロッターの1ステップ分進んだ。 | |
initial_state | ` | 0...0>` | 進化する QuantumCircuit 準備が整った。 この回路に、地元の特色を存分に盛り込んでください。 |
observables | サイトごと Z | 何でも引数 observables として EstimatorV2 受け付けます。 出力列ごとに1つの観測値。 | |
trotter_options | 2次のスズキ | {"method": ..., "synthesis_settings": {...}}. および reps は time 、その関数に属する。 | |
aqc_options | 説明を参照 | max_bond (32), cutoff (1e-8), autodiff_backend ("jax"), fidelity_target (None), optimizer_settings (L-BFGS-B, jac=True, maxiter=300). | |
estimator_options | DD、くるくる回る、TREX | EstimatorV2.options, そのまま可決された。 提供された辞書は、デフォルトの辞書と統合されるのではなく、デフォルトの辞書を全面的に置き換えます。 | |
transpiler_options | {"optimization_level": 3} | generate_preset_pass_manager キーワード引数。 backend および は、実行パスがこれらを所有しているため、拒否 target されます。 | |
backend | "runtime" | "statevector"、 "fake"、または "runtime"。 | |
backend_name | 最も空いている | IBM® のバックエンド名 runtime、または名前付きフェイクバックエンド。 | |
batches | 1 | 回路をN個の実行ジョブに分割します。 1回のバッチ処理では、1つのジョブのみを送信し、セッションは作成されません。 | |
parallel_sim | False | Ray を使用して、利用可能なすべてのコアにローカルシミュレータの処理パスを分散させます。 …には影響がない runtime。 | |
return_circuits | False | 結果として、観測可能な系列とともに、論理的なAQC+トロッター回路を返してください。 |
実行バックエンド
3つのパスすべてで、コードと緩和策の設定は共通しています。 両者の違いは、回路がどこを通っているかという点だけである。
backend | 説明 | 資格情報 | ノート |
|---|---|---|---|
"statevector" | 正確 StatevectorEstimator | サーバーレスアカウントのみ | 正確な参照パス。 QPUの時間がありません。 |
"fake" | Qiskitのフェイクバックエンドにおけるノイジーなローカルシミュレーション | サーバーレスアカウントのみ | 「緩和された runtime 道」を忠実に再現すること。 必要なもの qiskit-aer。 デフォルトでは127キュービットです fake_sherbrooke。 |
"runtime" (デフォルト) | 実際のQPU EstimatorV2 に対して緩和された | サーバーレスアカウントおよびQPUアクセス権限を持つインスタンス | backend_name オプション。これを省略すると、負荷が最も低いデバイスが選択されます。 |
どちらのシミュレータパスも、デプロイされた関数を呼び出すため、QPU時間を一切使用しない場合でも、保存済みのServerlessアカウントが必要となります。 以下の2つの例では、まず statevector 同じワークロードを上で実行し、次に上で実行します runtime。
出力
job.result() 単純な辞書を返します:
{
"times": [...], # length t_steps + 1, t_k = k * dt (t=0 is the prepared state)
"expectation_values": [[...]], # shape (n_times, n_observables)
"observable_labels": [...], # for example: ["Z_0", "ZZ_0_1"]
"metadata": {
"n", "t_steps", "dt", "tier",
"aqc_compressed_steps": 5, # total compressed steps (= sum of segment n_steps)
"aqc_segments": [ # per segment: the plan plus its own results
{"n_steps": 3, "ansatz_steps": 1, "steps": [1, 2, 3], "n_params": 133,
"fidelities": {"1": ..., "2": ..., "3": ...}},
{"n_steps": 2, "ansatz_steps": 2, "steps": [4, 5], "n_params": 245,
"fidelities": {"4": ..., "5": ...}},
],
"execution_backend",
"aqc_fidelities": {"1": ..., "2": ...}, # flat per-step fidelity, all compressed steps
"circuit_stats": { # per-step 2q depth and gate count, full Trotter vs AQC
"1": {"full_trotter": {"depth_2q": ..., "num_2q_gates": ...},
"aqc_trotter": {"depth_2q": ..., "num_2q_gates": ...}},
"2": {...},
},
"warnings": [...], # non-fatal notices; for example, a cotengrust fallback
"resource_usage": { # per stage; QPU_TIME is the charged QPU time
"RUNNING: OPTIMIZING_FOR_HARDWARE": {"CPU_TIME": ...},
"RUNNING: WAITING_FOR_QPU": {"CPU_TIME": ...},
"RUNNING: EXECUTING_QPU": {"QPU_TIME": ...},
},
},
# present only when return_circuits=True
"circuits": [QuantumCircuit, ...], # one per evolved step; circuits[i] is at times[i + 1]
}aqc_fidelities と は、まず最初に読むべき2つの項目 circuit_stats です。これらを併せて確認することで、圧縮が忠実に行われたか、そして実際に深度が保存されたかがわかります。 では runtime、課金対象となるQPU時間とは別に、キューの待機時間を報告 resource_usage します。 拒否された入力は、構造化された( ServerlessError コード 4615)として迅速に失敗します。
シミュレータの例
まず、その関数を正確にそのバック statevector エンド上で実行してください。 QPUのリソースを一切消費せず、デプロイメントをエンドツーエンドで検証します。 ここでのモデルは8キュービットの横磁場アイジング鎖であり、が省略 observables されているため、この関数はデフォルトのサイトごとの を測定する。
圧縮プランは、理解しておく価値のある情報です。 各セグメントでは、連続 k するトロッターステップを、- mステップのトロッターターゲットから構成されるアンザッツに圧縮 {"n_steps": k, "ansatz_steps": m} し、それを超えるステップは通常のトロッターとして実行 sum(n_steps) される。 初期の、エンタングルメントの低いステップは、浅い単層アンザッツにうまく圧縮されますが、後期の、よりエンタングルメントの高いステップには、より深いアンザッツが必要となります。
from qiskit.quantum_info import SparsePauliOp
fn = serverless.load("aqc-dynamics-function")
n = 8
H = SparsePauliOp.from_sparse_list(
[("ZZ", [i, i + 1], 1.0) for i in range(n - 1)]
+ [("X", [i], 0.8) for i in range(n)],
num_qubits=n,
)
job = fn.run(
t_steps=8,
aqc_segments=[
{
"n_steps": 4,
"ansatz_steps": 1,
}, # early steps -> shallow 1-layer ansatz
{
"n_steps": 2,
"ansatz_steps": 2,
}, # later steps -> deeper 2-layer ansatz
],
hamiltonian=H,
aqc_options={"max_bond": 32},
backend="statevector",
)
print("job ID:", job.job_id)Output:
job ID: ee1f3793-e995-427d-81d1-5924549beb38
レースの経過を追って、結果を確認してください
status() この関数は、ジョブのライフサイクル全体と、実行中に各ステージごとに公開されるサブステータスの両方を報告します。 このガイドの後半で説明するハードウェアの実行についても、同じ手順が適用されます:
QUEUED -> INITIALIZING -> RUNNING: OPTIMIZING_FOR_HARDWARE -> RUNNING: WAITING_FOR_QPU -> RUNNING: EXECUTING_QPU -> RUNNING: POST_PROCESSING -> DONE
status() 値 | ステージ |
|---|---|
RUNNING: OPTIMIZING_FOR_HARDWARE | 状態の準備、トロッター構築、AQC圧縮 |
RUNNING: WAITING_FOR_QPU | QPUにキューに入れられた(runtime バックエンドのみ) |
RUNNING: EXECUTING_QPU | 実行中の回路(ローカルシミュレータがこれを直接マークする) |
RUNNING: POST_PROCESSING | 結果辞書の構築 |
終端状態は DONE、 ERROR、、 です CANCELED。 この実行 statevector にはQPUキューがないため、をスキップします RUNNING: WAITING_FOR_QPU。 任意の job.logs() 時点で を使用すると、各ステップで達成されたAQCの忠実度を含む、ステージごとのログを確認できます。
print(job.status()) # re-run until this reports DONEOutput:
DONE
import numpy as np
result = job.result()
ev = np.array(result["expectation_values"])
print("observables:", result["observable_labels"])
print("shape:", ev.shape, "-> (n_times, n_observables)")
print("first row (t = 0, the prepared state):", np.round(ev[0], 4))
print("last row (t = t_steps * dt):", np.round(ev[-1], 4))
print(
"AQC fidelities:",
{k: round(v, 4) for k, v in result["metadata"]["aqc_fidelities"].items()},
)
# What the compression bought: 2-qubit depth at the final time step.
stats = result["metadata"]["circuit_stats"][
str(result["metadata"]["t_steps"])
]
print(
"2q depth at the final step:",
stats["full_trotter"]["depth_2q"],
"(full Trotter) ->",
stats["aqc_trotter"]["depth_2q"],
"(AQC + Trotter)",
)Output:
observables: ['Z_0', 'Z_1', 'Z_2', 'Z_3', 'Z_4', 'Z_5', 'Z_6', 'Z_7']
shape: (9, 8) -> (n_times, n_observables)
first row (t = 0, the prepared state): [1. 1. 1. 1. 1. 1. 1. 1.]
last row (t = t_steps * dt): [0.1442 0.2956 0.4686 0.4877 0.4869 0.4686 0.2963 0.1441]
AQC fidelities: {'1': 1.0, '2': 1.0, '3': 1.0, '4': 1.0, '5': 1.0, '6': 0.9999}
2q depth at the final step: 210 (full Trotter) -> 79 (AQC + Trotter)
ハードウェアの例
関数呼び出しは、実際の IBM Quantum プロセッサ上でトランス backend="runtime" パイルされ、実行されます。この際、関数に組み込まれたエラー緩和機能、すなわち動的デカップリング( XY4 )、ゲート・トゥワーリング、およびトゥワーリングされた読み出しエラー消去(TREX)が適用されます。 backend_name デバイスを指定します。指定しない場合は、関数が最も負荷の低いデバイスを選択します。
科学コードについては、変更点は一切ありません。 シミュレータの例との違いは、チェーンの長さ、トロッターステップの数、圧縮プラン、バックエンド、および次のセクションで説明する明示的な緩和設定にあります。
制御ハードウェアに必要な処理能力の算出
estimator_options その入力は、わざわざ設定する価値があるのでしょうか。 ゲートの回転処理では、 PUBnum_randomizations ごとに個別のランダム化された回路が構築され、すべてのランダム化を含む PUB を含む処理全体が、QPUの古典的制御システムの命令メモリに収まる必要があります。 この関数のデフォルト設定は1000回のランダム化であるため、10ステップの進化では、それぞれ1000個の回路からなる11個のPUBが送信されます。つまり、1つのジョブでおよそ11,000個の回路インスタンスが生成されることになります。
制御システムの許容範囲を超えると、ジョブはエラー 6073 で失敗します。 「ジョブ制限」 では、閾値とその算出方法が規定されています。主な制限として、クビットあたり 26.8 百万回の制御システム命令が挙げられ、これは PUB ごとではなく、ジョブごとに適用されます。 動的デカップリングでは、そのカウントに加算されるゲートが追加されます。
サイズを制御する入力は2つあります:
estimator_optionsショットの予算を設定します。 総ショット数は であるためnum_randomizations * shots_per_randomization、ランダム化の回数と1回のランダム化あたりのショット数をトレードオフさせ、統計情報を維持しつつ、プログラムのサイズを縮小することができます。 次のセルでは、各ランダム化につき200ショットのランダム化を100回行っています。これは、観測可能変数1つあたり20,000ショットに相当し、デフォルト設定で送信される回路インスタンス数の約10分の1となります。 すべてのフィールドの一覧については、「 TwirlingOptions 」および「 Estimator options」 を参照してください。batchesPUBをその数だけ個別のランタイムジョブに分割します。これは、エラー 6073 の解決策として示されている方法そのものであり、ジョブごとのフレーミングが重要となる理由でもあります。 この設定では、1回のジョブにつき11件のPUBを一括で送信するのではなく、およそ3件ずつ送信batches=4します。また、ジョブは1つのバッチとしてまとめて送信されるため、各ジョブが個別にキューに並ぶのではなく、グループ全体で1回だけキューに並ぶことになります。
指定された値は、関数のデフォルト値と統合されるのではなく、それらを全面的に上書き estimator_options することに注意してください。そのため、動的デカップリングとTREXについては、これらを有効な状態に保つために、次のセルで改めて設定しています。
from qiskit.quantum_info import SparsePauliOp
fn = serverless.load("aqc-dynamics-function")
n = 10
H = SparsePauliOp.from_sparse_list(
[("ZZ", [i, i + 1], 1.0) for i in range(n - 1)]
+ [("X", [i], 0.8) for i in range(n)],
num_qubits=n,
)
job = fn.run(
t_steps=10,
aqc_segments=[
{
"n_steps": 3,
"ansatz_steps": 1,
}, # early steps -> shallow 1-layer ansatz
{
"n_steps": 3,
"ansatz_steps": 2,
}, # later steps -> deeper 2-layer ansatz
],
hamiltonian=H,
aqc_options={"max_bond": 32},
backend="runtime",
backend_name="ibm_marrakesh",
# The function defaults to 1000 twirling randomizations, which was too large
# for this device. Total shots is num_randomizations *
# shots_per_randomization, so this is 20,000 shots per observable.
estimator_options={
"dynamical_decoupling": {"enable": True, "sequence_type": "XY4"},
"twirling": {
"enable_gates": True,
"num_randomizations": 100,
"shots_per_randomization": 200,
},
"resilience": {"measure_mitigation": True},
},
)
print("job ID (save this to reconnect later):", job.job_id)Output:
job ID (save this to reconnect later): 7229a8bf-9f83-4785-8dd4-489844abc2d9
ハードウェア実行には時間がかかり、その大部分はQPU上ではなく、従来通りの方法で処理されます。 AQC圧縮は、データがQPUに到達する前の段階で関数内部で実行され、その上にQPUキューが構築されます。 このノートブックやカーネルは、実行中は開いたままにしておく必要はありません。
前のセルに表示されているジョブIDをコピーして保存してください。 次の3つのセルでは、後で処理を再開できるように設定できます:
- 再接続(新しいカーネルセッションでのみ必要): 認証セルを再実行して再作成し
serverless、保存しておいたIDからハンドjobルを再構築してください。 送信したセッションがまだ有効な場合は、このセルをスキップしてください。ハンドルはすでに有効になっているためです。 - ステータスの確認:結果が表示されるまで再実行してください
DONE。 - 結果を取得する:ステータスが になったときにのみ実行する
DONE。
保存したIDを、以下の再接続用セルのプレースホルダーに貼り付けてください。
# Reconnect to a previously submitted job by its ID. Only needed in a NEW kernel
# session; if you are still in the session where you submitted, the `job` handle
# from the preceding cell is already live, so skip this cell. Replace the ID that follows with your own.
job = serverless.get_job_by_id("<your job ID>")# Re-run this until it reports DONE, then fetch the result in the following cell.
print(job.status())Output:
DONE
import numpy as np
# Run this only once the preceding status cell reports DONE. result() blocks until
# the job finishes, so calling it earlier just waits.
result = job.result()
ev = np.array(result["expectation_values"])
print("backend:", result["metadata"]["execution_backend"])
print("shape:", ev.shape, "-> (n_times, n_observables)")
print("last row (t = t_steps * dt):", np.round(ev[-1], 4))
print(
"AQC fidelities:",
{k: round(v, 4) for k, v in result["metadata"]["aqc_fidelities"].items()},
)
# What the compression bought: 2-qubit depth at the final time step.
stats = result["metadata"]["circuit_stats"][
str(result["metadata"]["t_steps"])
]
print(
"2q depth at the final step:",
stats["full_trotter"]["depth_2q"],
"(full Trotter) ->",
stats["aqc_trotter"]["depth_2q"],
"(AQC + Trotter)",
)Output:
backend: runtime
shape: (11, 10) -> (n_times, n_observables)
last row (t = t_steps * dt): [0.1504 0.1361 0.218 0.2144 0.2275 0.1783 0.1749 0.1599 0.0915 0.0922]
AQC fidelities: {'1': 1.0, '2': 1.0, '3': 1.0, '4': 1.0, '5': 0.9999, '6': 0.9999}
2q depth at the final step: 342 (full Trotter) -> 171 (AQC + Trotter)
次のステップ
- 「AQC + トロッター動力学を用いた中性子散乱のシミュレーション」というサーバーレスワークフローを順を追って実行してください。これは、 KCuF の動的構造因子を計算するために、このデプロイ済みの関数を呼び出す付随するサンプルです。
- 入力および出力の契約内容の詳細、その他の例、および引用情報については、 GitHub に掲載されている「AQC Dynamics」テンプレートをご覧ください。
- Qiskit Functionテンプレートリポジトリを閲覧して、同様の方法で作成された他のアプリケーションテンプレートを探してみてください。
- デプロイ済みの関数を管理するには、『 Qiskit Serverless 』ガイドをご覧ください。
- Qiskitアドオン「AQC-Tensor」 のドキュメントを参照して、AQC圧縮ステージについてさらに詳しく学びましょう。