דינמיקת ספין ומטען במודל פרמי-האברד
הערכת שימוש: 5 דקות על מעבד Heron r3. (הערה: זו הערכה בלבד. זמן הריצה שלכם עשוי להשתנות.)
רקע
מודל פרמי-האברד מתאר אלקטרונים שקופצים על סריג, עם קנס אנרגיה על תפוסה כפולה. בשרשרת חד-ממדית של אתרים עם תנאי שפה פתוחים, ההמילטוניאן מוגדר כך:
כאשר יוצר פרמיון בעל ספין באתר ו- סופר אותם. שלושת האיברים הם משרעת הקפיצה על הקשר בין האתרים ו-, האינטראקציה באתר , והפוטנציאל הכימי . המדריך מודד אנרגיה ביחידות של וזמן ביחידות של .
קשה לחשב באופן קלאסי דינמיקה בזמן אמת של המודל הזה. אלכסון מדויק נעצר בסביבות אתרים, ואחרי quench האנטרופיה של הסתבכות בפותר רשתות טנזורים גדלה ליניארית בזמן, ולכן מימד הקשר הנדרש לדיוק קבוע גדל אקספוננציאלית.
בממד אחד, העירורים היסודיים הם ספינונים, שנושאים ספין ואין להם מטען, והולונים, שנושאים מטען ואין להם ספין. השניים מתפשטים במהירויות שונות, ולכן הפרעה מקומית מתפצלת לחזית מהירה ולחזית איטית. האפקט הזה נקרא הפרדת ספין-מטען, והמדריך הזה מודד אותו [1].
המדריך הזה משתמש ב-Qiskit Function בשם Dynamics Simulator, שמקבלת את ההמילטוניאן, מצב התחלתי, לוח זמנים של Trotter ורשימת observables, ומחזירה ערכי תצפית. היא מטפלת פנימית במיפוי Jordan-Wigner, ב-Trotterization, בפריסת ה-qubits ובדיכוי שגיאות, ולכן אין במדריך הזה מעגלים לבנות או לעשות להם transpile.
דרישות
לפני שמתחילים את המדריך הזה, ודאו שהחבילות הבאות מותקנות:
-
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)
הגדרה
קודם מתחברים עם מפתח ה-API של IBM Quantum. אחר כך בוחרים את ה-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 אתרים עם אינטראקציה דוחה . פוטנציאל כימי אחיד תורם רק פאזה גלובלית כשמספר החלקיקים קבוע, ולכן בדוגמה הזאת מאפסים אותו. הפוטנציאל הכימי משנה רק כשהוא משתנה מאתר לאתר, למשל כדי להוסיף אי-סדר או אי-טהרה בודדת.
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)
הכנת המצב ההתחלתי
כל אתר מכיל שני אורביטלי ספין, ולכן שרשרת של אתרים משתמשת ב- qubits. המצב ההתחלתי הוא מחרוזת תפוסה של Fock עם תו אחד לכל אתר:
המחרוזת נקראת מהאתר הכי פחות משמעותי, ולכן התו הימני ביותר הוא אתר 0, ואינדקס האתר הפיזי של תו הנספר משמאל הוא . לכן המחרוזת udud אומרת שאתר 0 הוא d, אתר 1 הוא u, אתר 2 הוא d ואתר 3 הוא u.
בשלב הבא מכינים מצב Néel בחצי מילוי, עם u ו-d לסירוגין, כשהאתר המרכזי מוחלף בחלל ריק. החלל הריק הוא ההפרעה שאחריה עוקבים בזמן שהיא מתפשטת.
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
בניית ה-observables
כל observable הוא מילון שמייצג סכום משוקלל של מכפלות של אופרטורי תפוסה:
כל איבר ב-operators הוא איבר אחד , שנכתב כרשימה של זוגות [name, site] שמוכפלים יחד, והאיבר התואם ב-coefficients הוא המשקל שלו. שני השמות הם "Nup" עבור ו-"Ndn" עבור . observable חד-נקודתי בודד נכתב כך:
{"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
דרושות שלוש קבוצות של observables. צפיפות המטען והספין המקומי הם שניהם סכומים של שני איברים על אותם אופרטורים, ושונים רק במקדמים שלהם. מתאם הספין בין אתר לאתר החלל הריק מתפתח לארבעה איברים דו-נקודתיים עם מקדמים .
אופרטורי תפוסה מתחלפים, ולכן סדר הגורמים בתוך איבר אינו חשוב, אבל סדר הרשימה 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: אופטימיזציה של הבעיה להרצה על חומרה קוונטית
במדריך הזה אין שלב transpilation. הפונקציה מבצעת פנימית את השלבים הבאים [2], ואי אפשר להגדיר אותם מתוך המחברת:
-
ממפה את המודים הפרמיוניים ל-qubits לפי סדר שנבחר כדי לשמור על משקלי Pauli נמוכים על סריג heavy-hex
-
משתמשת ברשת SWAP פרמיונית כך שכל איבר קפיצה הופך לסיבוב דו-qubit
-
משבצת את השרשרת לאורך מסלול שנמנע מ-qubits חלשים
-
מפעילה dynamical decoupling ו-Pauli twirling בזמן ההידור
אתם בוחרים את ה-Backend ואת מספר ה-shots. הפונקציה מקבלת שם Backend כמחרוזת, ולכן המדריך הזה בוחר את ההתקן הכי פנוי שגדול מספיק לשרשרת ומעביר את השם שלו.
האות הקטן ביותר שצריך לפענח קובע את מספר ה-shots. עוקב המטען מגיע למשרעות של כ-, אבל עוקב הספין המחובר מגיע לשיא של כ-, קטן בערך פי ארבעה. המחקר שפורסם השתמש ב-20,000 shots לכל מעגל מהסיבה הזאת [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: הרצה באמצעות primitives של 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 שלו מאונדקס קודם לפי observable, בסדר שבו סופקו, ואחר כך לפי זמן. יש 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
הצגה חזותית של חזיתות הגל
אם משרטטים כל מתאם כמפת חום של מרחב וזמן, חרוט האור נראה ישירות. הזמן זורם כלפי מטה, והציר האופקי הוא המיקום לאורך השרשרת.
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)
המהירות המותאמת תלויה בסף הזיהוי ובקוונטיל החיתוך, והרגישות הזו דומיננטית על פני רעש הדגימה. סורקים את שניהם ומדווחים על החציון עם מעטפת מלאה, כמו בניתוח שפורסם.
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()
שרטוט הזיהויים בחזרה על מפות החום מראה אם האלגוריתם עקב אחרי הקצה המוביל. סמנים מלאים הם זיהויים שנכנסו להתאמה. סמנים ריקים נחתכו בגלל כללי המיסוך, או משום שהם קרובים מדי לאתר החסר, קרובים מדי לקצה השרשרת, או מאוחרים מספיק כך שהחזית התרחבה ומיקומה כבר אינו חד. הקו המקווקו הוא ההתאמה על פני הזמנים שבהם נעשה שימוש, והשיפוע שלו הוא המהירות בכותרת כל פאנל.
עמודת אתר החסר מסומנת מתחת לציר. המטען נחתך שם כי ערכו מסדר גודל של אחד, ולספין אין שם ערך, ולכן הוא מצויר באפור.
חרוט המטען נפתח רחב יותר מחרוט הספין. שתי ההפרעות עוזבות את אותו פגם באותו רגע ומתפשטות החוצה במהירויות שונות.
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()

חזית המטען עוקפת את חזית הספין. חור ברקע Mott חד-ממדי אינו נע כאלקטרון. במקום זאת הוא מתפצל להולון הנושא את המטען וספינון הנושא את הפרעת הספין, וב- הספינון הוא האיטי מבין השניים.
הסדר תלוי באינטראקציה. ב- המודל הוא פרמיונים חופשיים עם ו-, ולכן הספין מהיר יותר. דחייה מדכאת את הובלת הספין ומשאירה את הובלת המטען כמעט ללא שינוי, וב- ערכי החומרה שפורסמו הם ו- [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.