Langkau ke kandungan utama

Deploy dan jalankan templat Qiskit Function untuk dinamik Hamiltonian AQC + Trotter

Gambaran keseluruhan

Ini ialah templat Qiskit Function yang agnostik eksperimen untuk dinamik Hamiltonian. Diberikan Hamiltonian Pauli jiran terdekat 1D, keadaan awal yang disediakan (pilihan), dan set observable, ia menjalankan evolusi masa Trotter, mampatan Circuit pengkompilan kuantum anggaran (AQC), dan pelaksanaan yang dimitigasi, kemudian mengembalikan siri masa bagi setiap observable. Tukar persediaan (PRE) dan analisis (POST) dan teras yang sama memacu eksperimen yang berbeza:

PRE (persediaan anda)FUNCTION (dideploy di sini)POST (analisis anda)
Sediakan keadaan, sebagai Circuit atau keadaan produk, dengan tendangan tempatan pilihanSintesis Trotter → mampatan AQC → pelaksanaan pada statevector, fake, atau runtime, mengembalikan O(t)\langle O \rangle(t)S(q,ω)S(q, \omega) untuk penyerakan neutron, atau kemagnetan, pengangkutan, dinamik quench, dan sebagainya

Templat ini diterbitkan dalam repositori templat Qiskit Function, bersama-sama templat aplikasi lain. Notebook ini mendeploy ke akaun Qiskit Serverless anda sendiri. Jalankan sekali, dan mana-mana notebook kemudian boleh memanggil fungsi tersebut dengan serverless.load("aqc-dynamics-function").

Untuk contoh saintifik yang lengkap, lihat Simulasikan penyerakan neutron dengan aliran kerja Serverless dinamik AQC + Trotter, yang memanggil fungsi ini untuk mengira faktor struktur dinamik KCuF3_3. Notebook ini merangkumi deployment dan kontrak input sebaliknya.

Keperluan

Sebelum bermula, pastikan anda mempunyai yang berikut dalam persekitaran kernel notebook ini:

  • Qiskit SDK v2.0 atau lebih baharu (pip install qiskit).

  • Klien Qiskit IBM Catalog (pip install qiskit-ibm-catalog), yang mendeploy dan menjalankan beban kerja pada Qiskit Serverless.

Dependensi saintifik fungsi itu sendiri (qiskit-addon-aqc-tensor, cotengrust, qiskit-aer) tidak perlu dipasang secara lokal.

Dapatkan fail sumber templat

Fungsi ini ialah pakej Python kecil yang dijalankan oleh Qiskit Serverless dalam awan, jadi sumbernya perlu wujud sebagai fail tempatan yang dimuat naik semasa deploy. Pakej ini diterbitkan dalam repositori templat Qiskit Function.

Muat turun source_files

Muat turun tersebut ialah satu fail zip tunggal, dinamakan mengikut laluan penuh direktori dalam repositori:

qiskit-community qiskit-function-templates main physics aqc_trotter source_files.zip

  1. Nyahzip ke dalam direktori yang menyimpan notebook ini.

  2. Namakan semula folder yang diekstrak daripada nama panjang itu kepada source_files.

Direktori kerja anda kemudian akan kelihatan seperti ini:

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

Nama tersebut perlu tepat source_files, kerana itulah working_dir yang dimuat naik pada Langkah 3.

program.py ialah titik masuk yang dipanggil oleh gateway. Semua di bawah source/ ialah pelaksanaan, dibahagikan mengikut peringkat: sintesis Hamiltonian dan Trotter, mampatan AQC, dan pelaksanaan. Tiada satu pun yang perlu disunting untuk menjalankan contoh yang berikut. Langkah 3 memuat naik keseluruhan direktori, jadi ulangi langkah itu setiap kali anda menukar fail.

# Added by doQumentation — required packages for this notebook
!pip install -q numpy qiskit qiskit-ibm-catalog

1. Pengesahan

Guna qiskit-ibm-catalog untuk mengesahkan diri kepada QiskitServerless dengan kunci API (token) dan CRN (instance) anda, yang boleh anda dapati pada papan pemuka IBM Quantum® Platform. Dengan bukti kelayakan ini anda boleh menginstasiasi klien serverless secara tempatan untuk memuat naik atau menjalankan fungsi yang dipilih:

from qiskit_ibm_catalog import QiskitServerless
serverless = QiskitServerless(channel="ibm_quantum_platform", token="MY_TOKEN", instance="MY_CRN")

Anda boleh, secara pilihan, menggunakan save_account() untuk menyimpan bukti kelayakan anda dalam persekitaran tempatan anda (lihat panduan Sediakan akaun IBM Cloud® anda). Ambil perhatian bahawa ini menulis bukti kelayakan anda ke fail yang sama seperti QiskitRuntimeService.save_account():

QiskitServerless.save_account(channel="ibm_quantum_platform", token="MY_TOKEN", instance="MY_CRN")

Jika akaun telah disimpan, tiada keperluan untuk memberikan token untuk mengesahkan diri:

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. Isytiharkan dependensi

Pakej yang diperlukan oleh fungsi di atas imej serverless asas terurus.

nota

Gateway hanya memasang nama yang ada dalam senarai benarnya (requirements-dynamic-dependencies.txt), dipadankan mengikut nama pakej dan dipin kepada versi yang dibenarkan dengan ==. Apa-apa yang lain perlu tiba secara transitif (sebagai dependensi pakej yang disenaraikan). Sintaks [extras] dihormati: qiskit-addon-aqc-tensor[quimb-jax] ialah apa yang memasang quimb dan jax. cotengrust diperlukan untuk kecekapan memori semasa simulasi rangkaian tensor. qiskit-aer disenaraikan secara berasingan untuk Backend fake (simulasi hingar tempatan).

DEPENDENCIES = [
"qiskit-addon-aqc-tensor[quimb-jax]==0.3.1",
"qiskit-aer==0.17.2",
"cotengrust==0.2.0",
]

3. Takrifkan dan muat naik fungsi

from qiskit_ibm_catalog import QiskitFunction

fn = QiskitFunction(
title="aqc-dynamics-function",
entrypoint="program.py",
working_dir="source_files/",
dependencies=DEPENDENCIES,
)
serverless.upload(fn)
QiskitFunction(aqc-dynamics-function)

4. Sahkan ia telah didaftarkan

next(p for p in serverless.list() if p.title == "aqc-dynamics-function")
QiskitFunction(aqc-dynamics-function)

Rujukan fungsi

Ini ialah pengenalan ringkas. Setiap medan didokumentasikan sepenuhnya dalam README Templat Dinamik AQC: jadual input lengkap dengan peraturan pengesahannya, medan output, Backend pelaksanaan, dan contoh lengkap selanjutnya. Yang berikut ialah versi ringkas, cukup untuk membaca contoh yang berikut.

Input

Setiap larian ialah satu panggilan fn.run(...) tunggal. Hanya tiga input pertama dalam jadual diperlukan: hamiltonian, t_steps, dan aqc_segments. Semua selepasnya adalah pilihan dan kembali kepada lalai yang ditunjukkan, jadi panggilan minimum ialah tiga argumen dan selebihnya jadual ialah fungsi yang anda boleh pilih untuk digunakan. num_qubits bagi Hamiltonian menetapkan panjang rantai, jadi tiada input saiz berasingan.

InputLalaiKeterangan
hamiltoniandiperlukanHamiltonian Pauli jiran terdekat 1D sebagai SparsePauliOp. String ialah operator Pauli, jadi tiada faktor tersirat satu perdua.
t_stepsdiperlukanJumlah langkah Trotter. Berkembang kepada T = t_steps * dt dan melaporkan setiap observable pada setiap t_k = k * dt.
aqc_segmentsdiperlukanPelan mampatan: senarai {"n_steps": k, "ansatz_steps": m}. Langkah sum(n_steps) dimampatkan; selebihnya dijalankan sebagai Trotter biasa.
dt0.2Masa fizikal yang dimajukan oleh satu langkah Trotter.
initial_state|0...0>QuantumCircuit yang disediakan untuk berkembang. Bina sebarang tendangan tempatan ke dalam Circuit ini.
observablesZ per-tapakApa-apa yang diterima oleh EstimatorV2 sebagai argumen observables-nya. Satu observable bagi setiap lajur output.
trotter_optionsSuzuki peringkat ke-2{"method": ..., "synthesis_settings": {...}}. reps dan time dimiliki oleh fungsi.
aqc_optionslihat keteranganmax_bond (32), cutoff (1e-8), autodiff_backend ("jax"), fidelity_target (None), optimizer_settings (L-BFGS-B, jac=True, maxiter=300).
estimator_optionsDD, twirling, TREXEstimatorV2.options, dilalukan terus sebagaimana adanya. Kamus yang dibekalkan menggantikan lalai secara keseluruhan dan bukannya bercantum dengannya.
transpiler_options{"optimization_level": 3}Argumen kata kunci generate_preset_pass_manager. backend dan target ditolak, kerana laluan pelaksanaan memiliki keduanya.
backend"runtime""statevector", "fake", atau "runtime".
backend_namepaling tidak sibukNama Backend IBM® untuk runtime, atau Backend palsu yang dinamakan.
batches1Bahagikan Circuit merentasi N kerja runtime. Satu batch menghantar satu kerja tunggal dan tidak mencipta sesi.
parallel_simFalseKipaskan laluan simulator tempatan merentasi semua teras yang tersedia dengan Ray. Tiada kesan pada runtime.
return_circuitsFalseKembalikan Circuit logik AQC + Trotter dalam hasil bersama siri observable.

Backend pelaksanaan

Ketiga-tiga laluan berkongsi kod yang sama dan tetapan mitigasi yang sama. Ia hanya berbeza pada tempat Circuit dijalankan.

backendApa ituBukti kelayakanNota
"statevector"StatevectorEstimator yang tepatAkaun Serverless sahajaLaluan rujukan yang tepat. Tiada masa QPU.
"fake"Simulasi hingar tempatan pada Backend palsu QiskitAkaun Serverless sahajaLatihan yang setia bagi laluan runtime yang dimitigasi. Memerlukan qiskit-aer. Lalai kepada fake_sherbrooke 127-qubit.
"runtime" (lalai)EstimatorV2 yang dimitigasi terhadap QPU sebenarAkaun Serverless dan instance dengan akses QPUbackend_name pilihan; mengabaikannya memilih peranti paling tidak sibuk.

Kedua-dua laluan simulator masih memanggil fungsi yang dideploy, jadi ia memerlukan akaun Serverless yang disimpan walaupun ia tidak menggunakan masa QPU. Dua contoh yang berikut menjalankan beban kerja yang sama pada statevector dahulu, kemudian pada runtime.

Output

job.result() mengembalikan kamus biasa:

{
"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 dan circuit_stats ialah dua yang perlu dibaca dahulu: bersama-sama ia memberitahu anda sama ada mampatan kekal setia dan sama ada ia benar-benar menjimatkan kedalaman. Pada runtime, resource_usage melaporkan masa menunggu giliran secara berasingan daripada masa QPU yang anda dikenakan bayaran. Input yang ditolak gagal dengan pantas sebagai ServerlessError berstruktur (kod 4615).

Contoh simulator

Jalankan fungsi pada Backend statevector yang tepat dahulu. Ia tidak menggunakan masa QPU dan mengesahkan deployment dari hujung ke hujung. Model di sini ialah rantai Ising medan-transversal lapan qubit, dan observables diabaikan supaya fungsi mengukur ZZ per-tapak lalai.

Pelan mampatan ialah input yang berbaloi untuk difahami. Setiap segmen {"n_steps": k, "ansatz_steps": m} memampatkan k langkah Trotter berturut-turut ke dalam ansatz yang dibina daripada sasaran Trotter m-langkah, dan mana-mana langkah melebihi sum(n_steps) dijalankan sebagai Trotter biasa. Langkah awal berkekusutan rendah dimampatkan dengan baik ke dalam ansatz satu lapisan yang cetek; langkah kemudian yang lebih berkekusutan memerlukan yang lebih dalam.

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)
job ID: ee1f3793-e995-427d-81d1-5924549beb38

Ikuti larian dan baca hasil

status() melaporkan kedua-dua kitaran hayat kerja yang kasar dan sub-status setiap peringkat yang diterbitkan oleh fungsi semasa ia berjalan. Peringkat yang sama terpakai untuk larian perkakasan kemudian dalam panduan ini:

QUEUED -> INITIALIZING -> RUNNING: OPTIMIZING_FOR_HARDWARE -> RUNNING: WAITING_FOR_QPU -> RUNNING: EXECUTING_QPU -> RUNNING: POST_PROCESSING -> DONE

Nilai status()Peringkat
RUNNING: OPTIMIZING_FOR_HARDWAREpenyediaan keadaan, pembinaan Trotter, mampatan AQC
RUNNING: WAITING_FOR_QPUdalam giliran pada QPU (Backend runtime sahaja)
RUNNING: EXECUTING_QPUCircuit sedang dilaksanakan (simulator tempatan menandakan ini secara terus)
RUNNING: POST_PROCESSINGmenyusun kamus hasil

Keadaan terminal ialah DONE, ERROR, dan CANCELED. Larian statevector ini tiada giliran QPU, jadi ia melangkau RUNNING: WAITING_FOR_QPU. Guna job.logs() pada bila-bila masa untuk melihat log setiap peringkat, termasuk fideliti AQC yang dicapai pada setiap langkah.

print(job.status()) # re-run until this reports DONE
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)",
)
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)

Contoh perkakasan

Panggilan fungsi dengan backend="runtime" melakukan transpile dan melaksanakan pada pemproses IBM Quantum sebenar, dengan mitigasi ralat terbina dalam fungsi: penyahgandingan dinamik (XY4), twirling Gate, dan pemusnahan ralat pembacaan berpusing (TREX). backend_name memilih peranti; abaikan dan fungsi akan mengambil yang paling tidak sibuk.

Tiada apa-apa tentang kod sains yang berubah. Apa yang berbeza daripada contoh simulator ialah panjang rantai, bilangan langkah Trotter, pelan mampatan, Backend, dan tetapan mitigasi eksplisit yang dibincangkan dalam bahagian berikut.

Menyaiz kerja untuk perkakasan kawalan

estimator_options ialah input yang berbaloi untuk ditetapkan secara sengaja. Twirling Gate membina num_randomizations Circuit rawak berasingan untuk setiap PUB, dan keseluruhan kerja, setiap PUB dengan semua rawakannya, perlu muat dalam memori arahan sistem kawalan klasik QPU. Fungsi ini lalai kepada 1000 rawakan, jadi evolusi 10-langkah menghantar 11 PUB dengan 1000 Circuit setiap satu: lebih kurang 11,000 instance Circuit dalam satu kerja tunggal.

Melebihi apa yang dipegang oleh sistem kawalan dan kerja akan gagal dengan ralat 6073. Had kerja memberikan ambang dan cara mengiranya, yang utama ialah 26.8 juta arahan sistem kawalan bagi setiap qubit, dikenakan per kerja bukannya per PUB. Penyahgandingan dinamik menambah Gate yang dikira ke arahnya.

Dua input mengawal saiz:

  • estimator_options menetapkan bajet shot. Jumlah shot ialah num_randomizations * shots_per_randomization, jadi anda boleh menukar ganti rawakan dengan shot per rawakan, kekalkan statistik, dan masih mengecilkan program. Sel berikut menggunakan 100 rawakan pada 200 shot setiap satu, iaitu 20,000 shot bagi setiap observable dan lebih kurang satu persepuluh daripada instance Circuit yang akan dihantar oleh lalai. Lihat TwirlingOptions dan Pilihan Estimator untuk set penuh medan.

  • batches membahagikan PUB merentasi bilangan kerja runtime berasingan itu, yang merupakan penyelesaian yang dicadangkan oleh ralat 6073 itu sendiri dan sebab kerangka per kerja penting. Menetapkan batches=4 menghantar lebih kurang tiga PUB per kerja bukannya sebelas sekali gus, dan kerja-kerja tersebut dihantar bersama-sama dalam satu batch supaya kumpulan itu beratur sekali bukannya setiap kerja beratur secara berasingan.

Ingat bahawa estimator_options yang dibekalkan menggantikan lalai fungsi secara keseluruhan dan bukannya bercantum dengannya, jadi penyahgandingan dinamik dan TREX dinyatakan semula dalam sel berikut untuk mengekalkannya dihidupkan.

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)
job ID (save this to reconnect later): 7229a8bf-9f83-4785-8dd4-489844abc2d9
Menyambung semula kepada kerja yang berjalan lama

Larian perkakasan tidak pantas, dan kebanyakan masa adalah klasik dan bukannya pada QPU. Mampatan AQC berjalan dalam fungsi sebelum apa-apa sampai ke QPU, dan giliran QPU adalah tambahan kepada itu. Anda tidak perlu memastikan notebook atau kernel ini terbuka semasa ia berjalan.

Salin ID kerja yang dicetak oleh sel sebelumnya dan simpannya. Tiga sel berikutnya membolehkan anda menyambung semula larian kemudian:

  1. Sambung semula, hanya diperlukan dalam sesi kernel baharu: jalankan semula sel Pengesahan untuk mencipta semula serverless, kemudian bina semula pemegang job daripada ID yang anda simpan. Langkau sel ini jika anda masih dalam sesi di mana anda menghantar, kerana pemegang itu sudah aktif.

  2. Semak status: jalankan semula sehingga ia melaporkan DONE.

  3. Dapatkan hasil: jalankan hanya setelah status ialah DONE.

Tampal ID yang anda simpan menggantikan pemegang tempat dalam sel sambung semula berikut.

# 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())
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)",
)
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)

Langkah seterusnya

Cadangan