Fermi-Hubbard 모델의 스핀 및 전하 동역학
사용량 추정: Heron r3 프로세서에서 5분. (참고: 추정치일 뿐이며 실제 실행 시간은 다를 수 있습니다.)
배경
Fermi-Hubbard 모델은 전자가 격자 위에서 도약(hopping)하며 이중 점유에 에너지 페널티가 부과되는 상황을 기술합니다. 경계가 열린 개 사이트의 1차원 사슬에서 해밀토니안은 다음과 같이 정의됩니다.
여기서 는 사이트 에 스핀 의 페르미온을 생성하고, 는 그 개수를 셉니다. 세 항은 사이트 와 사이의 결합에서의 도약 진폭 , 온사이트 상호작용 , 화학 퍼텐셜 입니다. 이 튜토리얼에서는 에너지를 단위로, 시간을 단위로 측정합니다.
이 모델의 실시간 동역학은 고전적으로 계산하기 어렵습니다. 정확한 대각화는 사이트 근처에서 한계에 이르고, 퀀치(quench) 후에는 텐서 네트워크 솔버의 얽힘 엔트로피가 시간에 따라 선형적으로 증가하므로, 고정된 정확도에 필요한 결합 차원(bond dimension)이 지수적으로 증가합니다.
1차원에서 기본 들뜸은 스핀은 있으나 전하가 없는 스피논(spinon)과, 전하는 있으나 스핀이 없는 홀론(holon)입니다. 두 들뜸은 서로 다른 속도로 전파되므로, 국소적인 교란은 빠른 파면과 느린 파면으로 갈라집니다. 이 효과를 스핀-전하 분리라고 하며, 이 튜토리얼에서 이를 측정합니다 [1].
이 튜토리얼은 해밀토니안, 초기 상태, Trotter 스케줄, 관측량 목록을 받아 기댓값을 반환하는 Dynamics Simulator Qiskit Function을 사용합니다. Jordan-Wigner 매핑, Trotter 분해, 큐비트 레이아웃, 오류 억제를 내부에서 처리하므로, 이 튜토리얼에서는 직접 구성하거나 트랜스파일할 회로가 없습니다.
요구 사항
이 튜토리얼을 시작하기 전에 다음 패키지가 설치되어 있는지 확인하세요.
-
Qiskit SDK v2.3.0 이상 (
pip install qiskit) -
Qiskit Runtime v0.50.0 이상 (
pip install qiskit-ibm-runtime) -
Qiskit Functions Catalog v0.21.0 이상 (
pip install qiskit-ibm-catalog) -
NumPy v2.5.1 이상 (
pip install numpy) -
Matplotlib v3.10.3 이상 (
pip install matplotlib) -
Q-CTRL Visualizer v11.0.0 이상 (
pip install qctrl-visualizer)
설정
먼저 IBM Quantum API 키로 인증합니다. 그런 다음 다음과 같이 Qiskit Function을 선택합니다. (이 코드는 계정을 이미 저장하여 로컬 환경에 두었다고 가정합니다.)
# Added by doQumentation — installs the packages this notebook needs if they are missing
import importlib.util
_needed = {"matplotlib": "matplotlib", "numpy": "numpy", "qctrlvisualizer": "qctrlvisualizer", "qiskit_ibm_catalog": "qiskit-ibm-catalog", "qiskit_ibm_runtime": "qiskit-ibm-runtime", "scipy": "scipy"}
_missing = [pip for module, pip in _needed.items()
if importlib.util.find_spec(module) is None]
# One at a time, so a package that fails to install does not block the others
for _pip in _missing:
%pip install -q {_pip}
if not _missing:
print("\u2713 All required packages are installed")
import numpy as np
import matplotlib.pyplot as plt
from scipy import stats
from qiskit_ibm_catalog import QiskitFunctionsCatalog
from qiskit_ibm_runtime import QiskitRuntimeService
import qctrlvisualizer as qv
plt.style.use(qv.get_qctrl_style())
catalog = QiskitFunctionsCatalog(channel="ibm_quantum_platform")
# Access Function
sim_qf = catalog.load("q-ctrl/dynamics-simulator")
1단계: 고전적 입력을 양자 문제로 매핑
해밀토니안 정의
이 함수는 모델을 세 개의 키가 있는 딕셔너리로 받습니다. 각 키는 사슬 전체에 균일하게 적용되는 단일 float 값, 또는 사이트별로 값을 지정하는 리스트를 받습니다. 리스트는 hopping_amplitude에는 개 항목을 가지는데, 열린 사슬은 사이트보다 결합이 하나 적기 때문입니다. on_site_interaction과 chemical_potential에는 개 항목을 가집니다.
이 튜토리얼은 반발 상호작용 을 갖는 31개 사이트의 균일한 사슬을 사용합니다. 균일한 화학 퍼텐셜은 입자 수가 고정되어 있을 때 전역 위상만 더하므로, 이 예제에서는 0으로 설정합니다. 화학 퍼텐셜은 사이트마다 달라질 때, 예를 들어 무질서나 단일 불순물을 추가할 때에만 의미가 있습니다.
site_count = 31
hopping_amplitude = 1.0
on_site_interaction = 8.0
chemical_potential = 0.0
model = {
"hopping_amplitude": [hopping_amplitude] * (site_count - 1),
"on_site_interaction": [on_site_interaction] * site_count,
"chemical_potential": [chemical_potential] * site_count,
}
print(
f"{site_count} sites, {2 * site_count} qubits, U/t_h = {on_site_interaction:.0f}"
)
31 sites, 62 qubits, U/t_h = 8
Trotter 스케줄 설정
simulation 딕셔너리에는 시간 간격()과 Trotter 단계 수가 들어 있으며, 전체 진화 시간은 이 둘의 곱입니다. 단계가 클수록 회로 깊이를 고정한 채 더 나중 시간까지 도달할 수 있지만 Trotter 오차가 늘어나며, 이 오차는 해밀토니안 항들이 덜 교환하므로 에 따라 커집니다. 발표된 하드웨어 연구는 에 를 사용했으며 [2], 이 튜토리얼도 같은 값을 사용합니다. 회로 깊이는 단계 수에 비례해 선형적으로 증가하며 에는 전혀 의존하지 않습니다.
time_step = 0.15
step_count = 30
simulation = {"time_step": time_step, "step_count": step_count}
print(
f"Total evolution time: t = {time_step * step_count:.2f} (units of 1/t_h)"
)
Total evolution time: t = 4.50 (units of 1/t_h)
초기 상태 준비
각 사이트에는 두 개의 스핀 오비탈이 있으므로 개 사이트의 사슬은 개의 큐비트를 사용합니다. 초기 상태는 사이트당 한 문자로 이루어진 Fock 점유 문자열입니다.
문자열은 최하위 사이트부터 읽으므로 가장 오른쪽 문자가 사이트 0이며, 왼쪽에서 번째 문자에 해당하는 물리적 사이트 인덱스는 입니다. 따라서 문자열 udud는 사이트 0이 d, 사이트 1이 u, 사이트 2가 d, 사이트 3이 u라는 뜻입니다.
다음으로 u와 d가 번갈아 나타나는 반충전 Néel 상태를 준비하되, 중앙 사이트는 빈자리(vacancy)로 바꿉니다. 이 빈자리가 퍼져 나가는 모습을 추적할 교란입니다.
vacancy_site = site_count // 2
# Build in increasing site order, then reverse: the leftmost character of the
# string is the highest site index.
site_occupations = [
"u" if site % 2 == 0 else "d" for site in range(site_count)
]
site_occupations[vacancy_site] = "e"
initial_state = "".join(reversed(site_occupations))
print(f"Vacancy at site {vacancy_site}")
print(initial_state)
Vacancy at site 15
udududududududueudududududududu
관측량 구성
각 관측량은 점유 연산자들의 곱을 가중합으로 나타내는 딕셔너리입니다.
operators의 각 항목은 서로 곱해지는 [name, site] 쌍의 리스트로 쓰인 하나의 항 이고, coefficients의 대응하는 항목은 그 가중치입니다. 두 이름은 를 나타내는 "Nup"과 를 나타내는 "Ndn"입니다. 단일 1점 관측량 는 다음과 같이 씁니다.
{"operators": [[["Nup", 3]]], "coefficients": [1.0]}
다음 셀은 함수가 받아들이는 가장 작은 완전한 입력 집합을 출력하며, 이어지는 코드를 읽을 때 형태를 참고하는 데 유용합니다.
minimal_inputs = {
"model": {
"hopping_amplitude": 1.0,
"on_site_interaction": [2.0, 2.5],
"chemical_potential": 0.5,
},
"simulation": {"step_count": 5, "time_step": 0.1},
"observables": [{"operators": [[["Nup", 0]]], "coefficients": [1.0]}],
"initial_state": "ud",
"shots": 4096,
}
for key, value in minimal_inputs.items():
print(f"{key}: {value}")
model: {'hopping_amplitude': 1.0, 'on_site_interaction': [2.0, 2.5], 'chemical_potential': 0.5}
simulation: {'step_count': 5, 'time_step': 0.1}
observables: [{'operators': [[['Nup', 0]]], 'coefficients': [1.0]}]
initial_state: ud
shots: 4096
세 그룹의 관측량이 필요합니다. 전하 밀도 와 국소 스핀 는 모두 같은 연산자에 대한 두 항의 합이며 계수만 다릅니다. 사이트 와 빈자리 사이트 사이의 스핀 상관 는 계수가 인 네 개의 2점 항으로 전개됩니다.
점유 연산자는 서로 교환하므로 한 항 안에서 인자의 순서는 중요하지 않지만, observables 리스트의 순서가 반환되는 값의 순서를 결정합니다. 코드는 리스트를 만들면서 각 그룹의 인덱스를 기록합니다. 상관 함수는 곱이 인자를 반복하게 되는 빈자리 사이트를 건너뜁니다.
observables = []
charge_indices = []
spin_indices = []
correlator_indices = []
# Charge density: n_i = n_{i,up} + n_{i,down}
for site in range(site_count):
charge_indices.append(len(observables))
observables.append(
{
"operators": [[["Nup", site]], [["Ndn", site]]],
"coefficients": [1.0, 1.0],
}
)
# Local spin: S^z_i = (n_{i,up} - n_{i,down}) / 2
for site in range(site_count):
spin_indices.append(len(observables))
observables.append(
{
"operators": [[["Nup", site]], [["Ndn", site]]],
"coefficients": [0.5, -0.5],
}
)
# Spin correlator: S^z_i S^z_{i_*}, expanded into four two-point terms.
correlator_sites = [
site for site in range(site_count) if site != vacancy_site
]
for site in correlator_sites:
correlator_indices.append(len(observables))
observables.append(
{
"operators": [
[["Nup", site], ["Nup", vacancy_site]],
[["Nup", site], ["Ndn", vacancy_site]],
[["Ndn", site], ["Nup", vacancy_site]],
[["Ndn", site], ["Ndn", vacancy_site]],
],
"coefficients": [0.25, -0.25, -0.25, 0.25],
}
)
print(f"{len(observables)} observables")
print("Example correlator:", observables[correlator_indices[0]])
92 observables
Example correlator: {'operators': [[['Nup', 0], ['Nup', 15]], [['Nup', 0], ['Ndn', 15]], [['Ndn', 0], ['Nup', 15]], [['Ndn', 0], ['Ndn', 15]]], 'coefficients': [0.25, -0.25, -0.25, 0.25]}
2단계: 양자 하드웨어 실행에 맞게 문제 최적화
이 튜토리얼에는 트랜스파일 단계가 없습니다. 함수가 다음 단계를 내부적으로 수행하며 [2], 노트북에서는 이를 설정할 수 없습니다.
-
heavy-hex 격자에서 Pauli 가중치가 낮게 유지되도록 선택된 순서로 페르미온 모드를 큐비트에 매핑합니다
-
모든 도약 항이 2큐비트 회전이 되도록 페르미온 SWAP 네트워크를 사용합니다
-
약한 큐비트를 피하는 경로를 따라 사슬을 임베딩합니다
-
컴파일 중에 동적 디커플링과 Pauli 트월링을 적용합니다
백엔드와 샷 수는 직접 선택합니다. 이 함수는 백엔드 이름을 문자열로 받으므로, 이 튜토리얼에서는 사슬에 충분한 크기를 가진 가장 한가한 장치를 선택하고 그 이름을 전달합니다.
분해해야 하는 가장 작은 신호가 샷 수를 결정합니다. 전하 추적자는 약 의 진폭에 도달하지만, 연결된 스핀 추적자는 약 에서 정점을 이루어 대략 4배 더 작습니다. 이 때문에 발표된 연구에서는 회로당 20,000샷을 사용했습니다 [2].
service = QiskitRuntimeService()
backend = service.backend("ibm_boston")
backend_name = backend.name
shot_count = 20_000
print(f"{backend_name}, {shot_count} shots per circuit")
ibm_boston, 20000 shots per circuit
3단계: Qiskit 프리미티브를 사용하여 실행
다음으로 시뮬레이션을 제출합니다. run은 대기하지 않고 즉시 작업 핸들을 반환합니다.
job = sim_qf.run(
model=model,
simulation=simulation,
observables=observables,
initial_state=initial_state,
shots=shot_count,
backend_name=backend_name,
)
print(job.job_id, job.status())
6261b073-be19-48a1-be60-2f369b25613c QUEUED
job.status()로 진행 상황을 확인하고, job.result()로 작업이 끝날 때까지 대기합니다. 결과는 매핑이며, 그 observable_expectation_values 항목은 먼저 제공된 순서대로 관측량별로, 그다음 시간별로 인덱싱됩니다. 의 값이 포함되므로 시간 지점은 step_count + 1개입니다.
result = job.result()
print("Result keys:", sorted(result.keys()))
Result keys: ['observable_expectation_values', 'observables', 'provider_job_ids', 'warnings']
4단계: 후처리하여 원하는 고전적 형식으로 결과 반환
상관 함수 재구성
앞서 기록한 인덱스를 사용해 평평한 배열을 세 그룹으로 다시 나눈 다음, 두 개의 추적자 상관 함수를 구성합니다. 전하 추적자는 대비 사이트별 밀도의 변화입니다.
스핀 추적자는 각 사이트와 빈자리 사이트 사이의 연결된 상관 함수입니다.
둘 다 Fock 기저 연산자로 구성되므로 같은 측정 비트열에서 얻어지며, 두 번째 것은 추가 하드웨어 실행 비용이 들지 않습니다.
관측량을 구성할 때 코드는 전하(charge_indices), 스핀(spin_indices), 스핀 상관(correlator_indices) 연산자 각각의 리스트 내 위치를 기록했습니다. 시뮬레이터가 기댓값을 같은 순서로 반환하므로, 이 위치를 사용해 해당하는 결과를 추출할 수 있습니다. 스핀 상관의 경우 코드는 correlator_sites도 사용해 각 결과를 올바른 격자 위치에 배치하고, 빈자리는 NaN으로 남겨 둡니다.
expectation_values = np.asarray(
result["observable_expectation_values"], dtype=float
)
times = np.arange(expectation_values.shape[1]) * time_step
charge_density = expectation_values[charge_indices]
local_spin = expectation_values[spin_indices]
spin_correlator = np.full_like(charge_density, np.nan)
spin_correlator[correlator_sites] = expectation_values[correlator_indices]
# Charge tracer: change in per-site density relative to t = 0.
charge_tracer = charge_density - charge_density[:, [0]]
# Spin tracer: connected correlator with the vacancy site.
spin_tracer = 4.0 * (spin_correlator - local_spin * local_spin[vacancy_site])
spin_tracer[vacancy_site] = np.nan
print(
f"{expectation_values.shape[0]} observables, "
f"{expectation_values.shape[1]} time points up to t = {times[-1]:.2f}"
)
print(
f"Peak amplitudes: charge {np.nanmax(np.abs(charge_tracer)):.3f}, "
f"spin {np.nanmax(np.abs(spin_tracer)):.3f}"
)
92 observables, 31 time points up to t = 4.50
Peak amplitudes: charge 0.978, spin 0.168
파면 시각화
각 상관 함수를 공간과 시간의 히트맵으로 그리면 광원뿔(light cone)이 바로 보입니다. 시간은 아래쪽으로 흐르고, 가로축은 사슬을 따른 위치입니다.
def plot_tracer(axis, tracer, title):
"""Draw one tracer correlator as a space-time heatmap, time flowing down."""
# Scale from the off-centre sites. The vacancy site carries an order-one
# change in charge density and is left to clip rather than set the scale.
off_center = np.ones(site_count, dtype=bool)
off_center[vacancy_site] = False
limit = np.nanpercentile(np.abs(tracer[off_center]), 98)
colormap = qv.QCTRL_DIVERGENT_COLORMAP.reversed().copy()
colormap.set_bad("0.82") # the spin tracer has no value at the vacancy
image = axis.imshow(
tracer.T,
aspect="auto",
origin="upper",
cmap=colormap,
vmin=-limit,
vmax=limit,
extent=(-0.5, site_count - 0.5, times[-1], times[0]),
interpolation="nearest",
)
axis.annotate(
"vacancy",
xy=(vacancy_site, times[-1]),
xytext=(vacancy_site, times[-1] + 0.55),
ha="center",
va="top",
fontsize=8,
annotation_clip=False,
arrowprops=dict(arrowstyle="-", lw=0.8, color="0.4"),
)
axis.set_title(title)
axis.set_xlabel("Site")
return image
fig, axes = plt.subplots(1, 2, figsize=(11, 4.5), sharey=True)
charge_image = plot_tracer(
axes[0], charge_tracer, r"Charge tracer $C^{c}_{i}(t)$"
)
spin_image = plot_tracer(axes[1], spin_tracer, r"Spin tracer $C^{s}_{i}(t)$")
axes[0].set_ylabel(r"Time $t$ $(1/t_h)$")
fig.colorbar(charge_image, ax=axes[0], fraction=0.046)
fig.colorbar(spin_image, ax=axes[1], fraction=0.046)
fig.suptitle(f"$L$ = {site_count}, $U/t_h$ = {on_site_interaction:.0f}")
fig.tight_layout()
plt.show()

파면 속도 추출
발표된 연구 [2]의 검출 프로토콜을 따라 히트맵을 숫자로 바꿉니다. 각 시간 조각에서 빈자리를 중심으로 공간 프로파일을 대칭화하여 하드웨어 노이즈의 반대칭 부분을 평균해 없애고, 중심에서 벗어난 가장 큰 진폭으로 정규화한 다음, 임계값을 넘는 가장 바깥쪽 사이트를 기록합니다. 그런 다음 검출 결과를 연속된 시간 단계의 구간으로 나누고, 가장 긴 구간만 피팅하여 초기 과도 구간이 결과에 들어가지 않게 합니다.
def front_distance(profile, center, threshold):
"""Distance from `center` to the outermost site above a relative threshold."""
values = np.nan_to_num(np.asarray(profile, dtype=float))
length = values.size
symmetrized = values.copy()
for site in range(length):
mirror = 2 * center - site
if 0 <= mirror < length:
symmetrized[site] = 0.5 * (values[site] + values[mirror])
amplitude = np.abs(symmetrized)
off_center = np.ones(length, dtype=bool)
off_center[center] = False
peak = amplitude[off_center].max()
if peak <= 0.0:
return np.nan
above = np.flatnonzero((amplitude / peak > threshold) & off_center)
return float(np.abs(above - center).max()) if above.size else np.nan
def longest_run(mask):
"""Indices of the longest run of consecutive True values."""
best, current = [], []
for index, keep in enumerate(mask):
current = current + [index] if keep else []
if len(current) > len(best):
best = current
return np.array(best, dtype=int)
def front_velocity(tracer, center, threshold=0.3, quantile=0.75, min_block=5):
"""Fit front position against time, using only the longest contiguous run."""
distances = np.array(
[
front_distance(tracer[:, k], center, threshold)
for k in range(tracer.shape[1])
]
)
edge = max(center, tracer.shape[0] - 1 - center)
usable = (
np.isfinite(distances) & (distances >= 2) & (distances <= edge - 2)
)
block = longest_run(usable)
if block.size < min_block:
return np.nan, distances, np.array([], dtype=int), np.nan
candidates = [
stats.theilslopes(distances[block[:k]], times[block[:k]]).slope
for k in range(min_block, block.size + 1)
]
cutoff = np.quantile(candidates, quantile)
keep = max(i for i, value in enumerate(candidates) if value >= cutoff)
fit_block = block[: keep + min_block]
slope, intercept = np.polyfit(times[fit_block], distances[fit_block], 1)
return float(slope), distances, fit_block, float(intercept)
피팅된 속도는 검출 임계값과 트리밍 분위수에 의존하며, 이 민감도가 샷 노이즈보다 지배적입니다. 발표된 분석처럼 두 값을 모두 훑어 보고, 전체 범위(envelope)와 함께 중앙값을 보고하세요.
thresholds = [0.15, 0.225, 0.30, 0.375, 0.45]
quantiles = [0.60, 0.675, 0.75, 0.825, 0.90]
def velocity_envelope(tracer, center):
"""Median, minimum, and maximum velocity over the parameter scan."""
values = [
front_velocity(tracer, center, threshold=p, quantile=q)[0]
for p in thresholds
for q in quantiles
]
values = np.array([v for v in values if np.isfinite(v)])
if values.size == 0:
return np.nan, np.nan, np.nan
return float(np.median(values)), float(values.min()), float(values.max())
charge_velocity, charge_low, charge_high = velocity_envelope(
charge_tracer, vacancy_site
)
spin_velocity, spin_low, spin_high = velocity_envelope(
spin_tracer, vacancy_site
)
_, charge_front, charge_block, charge_intercept = front_velocity(
charge_tracer, vacancy_site
)
_, spin_front, spin_block, spin_intercept = front_velocity(
spin_tracer, vacancy_site
)
print(f"v_c = {charge_velocity:.2f} [{charge_low:.2f}, {charge_high:.2f}]")
print(f"v_s = {spin_velocity:.2f} [{spin_low:.2f}, {spin_high:.2f}]")
print(f"v_s / v_c = {spin_velocity / charge_velocity:.2f}")
v_c = 1.93 [1.81, 2.18]
v_s = 1.61 [1.23, 2.12]
v_s / v_c = 0.83
PUBLISHED = {"charge": 1.78, "spin": 1.46}
edge = max(vacancy_site, site_count - 1 - vacancy_site)
fig, axis = plt.subplots(figsize=(7, 4.6))
axis.axhspan(0, 2, color="0.92", zorder=0)
axis.axhspan(edge - 2, edge + 2, color="0.92", zorder=0)
series = [
(
"charge",
"$v_c$",
charge_front,
charge_block,
charge_velocity,
charge_intercept,
charge_low,
charge_high,
"C0",
"o",
),
(
"spin",
"$v_s$",
spin_front,
spin_block,
spin_velocity,
spin_intercept,
spin_low,
spin_high,
"C1",
"s",
),
]
for name, sym, front, block, v, b, lo, hi, color, marker in series:
excluded = np.setdiff1d(np.arange(front.size), block)
axis.plot(
times[excluded],
front[excluded],
marker,
color=color,
markersize=5,
markerfacecolor="none",
alpha=0.45,
zorder=2,
)
axis.plot(
times[block],
front[block],
marker,
color=color,
markersize=5,
label=f"{name}, {sym} = {v:.2f} [{lo:.2f}, {hi:.2f}]",
zorder=3,
)
span = times[block]
anchor = v * span[0] + b
axis.plot(
span,
v * (span - span[0]) + anchor,
"-",
color=color,
lw=1.6,
zorder=4,
)
axis.fill_between(
span,
lo * (span - span[0]) + anchor,
hi * (span - span[0]) + anchor,
color=color,
alpha=0.15,
zorder=1,
)
axis.plot(
span,
PUBLISHED[name] * (span - span[0]) + anchor,
"--",
color=color,
lw=1.2,
alpha=0.8,
zorder=4,
)
axis.plot([], [], "--", color="0.4", lw=1.2, label="published [2]")
axis.plot(
[],
[],
"o",
color="0.5",
markerfacecolor="none",
label="excluded from fit",
)
axis.set_xlabel(r"Time $t$ $(1/t_h)$")
axis.set_ylabel("Front distance from vacancy (sites)")
axis.set_xlim(0, times[-1])
axis.set_ylim(
0, min(edge + 1, np.nanmax(np.r_[charge_front, spin_front]) + 3)
)
axis.legend(loc="upper left", frameon=False, fontsize=9)
axis.set_title(
f"L = {site_count}, $U/t_h$ = {on_site_interaction:.0f}, front propagation",
fontsize=11,
)
fig.tight_layout()
plt.show()
검출 결과를 히트맵 위에 다시 그려 보면 알고리즘이 선두 경계를 제대로 추적했는지 알 수 있습니다. 채워진 마커는 피팅에 사용된 검출입니다. 열린 마커는 마스킹 규칙에 의해 제외되었는데, 빈자리에 너무 가깝거나, 사슬 끝에 너무 가깝거나, 파면이 퍼져 위치가 더 이상 선명하지 않을 만큼 늦은 시점이기 때문입니다. 점선은 사용된 시간 구간에 대한 피팅이며, 그 기울기가 각 패널 제목의 속도입니다.
빈자리 열은 축 아래에 표시되어 있습니다. 전하는 값이 1 정도 크기라서 거기서 잘리고, 스핀은 거기에 값이 없으므로 회색으로 그려집니다.
전하 원뿔이 스핀 원뿔보다 넓게 열립니다. 두 교란은 같은 순간에 같은 결함에서 출발하여 서로 다른 속도로 바깥으로 퍼져 나갑니다.
def plot_tracer_front(axis, tracer, front, block, velocity, intercept, title):
"""Overlay the detected wavefront and the fitted cone on a tracer heatmap."""
image = plot_tracer(axis, tracer, title)
excluded = np.setdiff1d(np.arange(front.size), block)
for index, filled in ((block, True), (excluded, False)):
good = index[np.isfinite(front[index])]
if good.size == 0:
continue
for sign in (-1, 1):
axis.plot(
vacancy_site + sign * front[good],
times[good],
"o",
markersize=4,
color="k",
markerfacecolor="k" if filled else "none",
markeredgewidth=0.9,
alpha=1.0 if filled else 0.4,
)
span = times[block]
for sign in (-1, 1):
axis.plot(
vacancy_site + sign * (velocity * span + intercept),
span,
"--",
color="k",
lw=1.5,
)
axis.set_title(f"{title} $v$ = {velocity:.2f}")
return image
fig, axes = plt.subplots(1, 2, figsize=(11, 4.5), sharey=True)
charge_image = plot_tracer_front(
axes[0],
charge_tracer,
charge_front,
charge_block,
charge_velocity,
charge_intercept,
r"Charge tracer $C^{c}_{i}(t)$",
)
spin_image = plot_tracer_front(
axes[1],
spin_tracer,
spin_front,
spin_block,
spin_velocity,
spin_intercept,
r"Spin tracer $C^{s}_{i}(t)$",
)
axes[0].set_ylabel(r"Time $t$ $(1/t_h)$")
fig.colorbar(charge_image, ax=axes[0], fraction=0.046)
fig.colorbar(spin_image, ax=axes[1], fraction=0.046)
fig.suptitle(f"$L$ = {site_count}, $U/t_h$ = {on_site_interaction:.0f}")
fig.tight_layout()
plt.show()

전하 파면이 스핀 파면을 앞지릅니다. 1차원 Mott 배경에서 홀(hole)은 전자처럼 이동하지 않습니다. 대신 전하를 나르는 홀론과 스핀 교란을 나르는 스피논으로 분수화되며, 에서는 스피논이 둘 중 더 느립니다.
이 순서는 상호작용에 따라 달라집니다. 에서 모델은 , 인 자유 페르미온이므로 스핀이 더 빠릅니다. 반발력은 전하 수송은 거의 그대로 두면서 스핀 수송을 억제하며, 에서 발표된 하드웨어 값은 과 입니다 [2]. 여러 값으로 다시 실행하고 를 그리면 교차 지점을 파악할 수 있습니다.
이 수치에 대한 주의 사항입니다. 임계값 기반 검출은 약간 낮게 편향되므로, 전하 속도가 발표된 값보다 몇 퍼센트 낮게 나오는 것은 예상된 결과입니다. 연결된 상관 함수는 노이즈 바닥에 더 가깝기 때문에 스핀 범위가 넓으며, 이 때문에 단일 피팅이 아니라 스캔의 중앙값을 제시합니다.
참고 문헌
[1] Essler, F. H. L., Frahm, H., Gohmann, F., Klumper, A., & Korepin, V. E. (2005). The One-Dimensional Hubbard Model. Cambridge University Press.
[2] Hartnett, G. S., Najafi, K. S., Khindanov, A., Liao, H., Schutzman, M., Hush, M. R., Biercuk, M. J., & Baum, Y. (2026). Fast, accurate, high-resolution simulation of large-scale Fermi-Hubbard models on a digital quantum processor. arXiv:2605.04025.