חזרה לבלוג

IBM Quantum · Qiskit · מחקר קוונטי · 2026-07-24 · 30 דקות קריאה

מאת נתנאל סיבוני

IBM Quantum בחינם: כך הרצתי מחקר אמיתי עם Qiskit על מחשב בעל 156 קיוביטים

התחלתי מרעיון מתמטי על התאבכות מסדר גבוה, אבל לפני שנגעתי בחומרה העברתי אותו ביקורת ספרות, ביקורת מתמטית, סימולציה, 56 בדיקות ו־preflight. לבסוף הרצתי Job יחיד על ibm_kingston, השתמשתי בשלושה קיוביטים פיזיים מתוך מערכת בעלת 156, וקיבלתי את כל הנתונים — יחד עם מגבלה שמנעה ממני לטעון מסקנה חזקה מדי.

התשובה הקצרה:
הקמתי סביבת מחקר קוונטית מאפס, חיברתי אותה ל־IBM Quantum, בדקתי את הרעיון מתמטית ומול הספרות, פסלתי תכנון ניסוי שהיה עלול לייצר תוצאה מזויפת, בניתי תבנית Qiskit אחידה, והרצתי Job יחיד על ibm_kingston — מערכת בעלת 156 קיוביטים. הניסוי עצמו השתמש בשלושה קיוביטים פיזיים, כלל 88 יחידות עבודה, 448 תצורות אפקטיביות ו־458,752 מדידות, וצרך 123 שניות QPU מתוך מכסת Open Plan החינמית. כל חמשת רווחי הסמך כללו אפס; לא נמצאה ראיה מובהקת להתאבכות מסדר גבוה. עם זאת, הסיווג המדעי נשאר inconclusive, משום ש־IBM החזירה חלון ביצוע מצטבר ולא חותמת זמן נפרדת לכל יחידת עבודה. המאמר הזה מציג את כל הדרך — התקנה, קוד, טעויות, בקרות, תוצאות ומגבלות — בלי לייפות דבר ובלי לחשוף סודות גישה.
המחשה של סביבת מחשוב קוונטי, מעגלי Qiskit וניתוח תוצאות ניסוי
מהרעיון המתמטי, דרך Qiskit ותכנון תקציב ה־QPU, ועד להרצה אמיתית ולמסקנה המדעית הזהירה.

תוכן עניינים

  1. למה בניתי את הניסוי הזה
  2. מחשוב קוונטי בשפה פשוטה
  3. IBM Quantum ועשר דקות QPU בחינם
  4. התקנת Python, Qiskit והסביבה
  5. התחברות בטוחה והתקלה הראשונה
  6. בדיקה מקומית לפני חומרה
  7. הרעיון, ביקורת הספרות וביקורת המתמטיקה
  8. מה באמת ביקשנו לבדוק
  9. המעגל הראשון שנפסל
  10. התכנון הפרמטרי המתוקן
  11. בחירת backend ו־transpilation
  12. Preflight, תקציב ואישור אנושי
  13. 88 PUBs וה־Job האמיתי
  14. Bootstrap, Holm והתוצאות
  15. למה התוצאה היא inconclusive
  16. איך לשחזר ומה אשנה בהרצה הבאה
  17. מקורות רשמיים
  18. שאלות נפוצות

למה בניתי את הניסוי הזה

קל לכתוב שמחשוב קוונטי הוא העתיד. קל עוד יותר לצלם מסך של Dashboard, להריץ מעגל לדוגמה ולהכריז שעבדנו עם מחשב קוונטי. אותי עניין מבחן קשה יותר: האם אני מסוגל לקחת רעיון תיאורטי לא מסודר, להפריד בו בין מתמטיקה ידועה להשערה חדשה, להפוך אותו לפרוטוקול שניתן להפרכה, לבנות קוד שאפשר לבדוק, לנהל מכסה יקרה ומוגבלת, ולפרסם גם את החלקים שלא הסתדרו?

זו בדיוק אותה יכולת שנדרשת בפרויקטי AI, אוטומציה ותשתיות. הטכנולוגיה משתנה, אבל המשמעת נשארת: להבין את הבעיה, להגדיר מה נחשב הצלחה, לזהות גורמים מבלבלים, להגביל הרשאות ועלויות, להקפיא גרסאות, לשמור ראיות ולסרב להסיק מסקנה שאינה נתמכת בנתונים. לכן המאמר הזה אינו “שלום עולם” של Qiskit. הוא תיעוד הנדסי ומדעי מקצה לקצה.

נקודת המוצא הייתה מסגרת רעיונית שקראתי לה HQC. במקום להתאהב בניסוח המקורי, שמרתי אותו, חישבתי לו hash, העברתי אותו ביקורת מתמטית וביקורת ספרות, ותיקנתי טענות רחבות מדי. רק לאחר מכן שאלתי מה מחשב קוונטי סטנדרטי של IBM באמת יכול לבדוק. התשובה הייתה צנועה: הוא אינו יכול להוכיח תיאוריה חדשה של פיזיקה; הוא יכול לשמש אמת מידה ניסויית למערכת הבקרה, לרצפת הרעש ולצינור הניתוח.

העיקרון שהוביל את העבודה:
מספר גדול של קיוביטים אינו הישג בפני עצמו. הישג הוא לבחור את מספר הקיוביטים הקטן הדרוש, לתכנן ניסוי שבו הרעש אינו מחקה את האפקט המבוקש, ולדעת מתי הנתונים אינם מספיקים למסקנה.
156קיוביטים ב־backend
3קיוביטים ששימשו בפועל
88PUBs שהתקבלו במלואם
448תצורות אפקטיביות
458,752shots בסך הכול
123 שניותזמן QPU שחויב

מחשוב קוונטי בשפה פשוטה: שישה מושגים שצריך להכיר

קיוביט הוא יחידת המידע הבסיסית של מחשב קוונטי. ביט רגיל הוא 0 או 1. מצב קוונטי יכול להיות צירוף של 0 ו־1 עד לרגע המדידה. אין פירוש הדבר שאפשר לקרוא את שתי התשובות יחד; המדידה מחזירה תוצאה קלאסית, והמידע נמצא בהתפלגות שמתקבלת כאשר חוזרים על הניסוי פעמים רבות.

סופרפוזיציה היא היכולת להכין צירוף של מצבים. שזירה היא תלות קוונטית בין קיוביטים שאינה מתפרקת לשתי מערכות עצמאיות. שער קוונטי הוא פעולה שמשנה את המצב, בדומה לפעולה לוגית במחשב רגיל, אבל על אמפליטודות קוונטיות. רצף השערים, יחד עם המדידות, נקרא מעגל קוונטי.

Shot הוא ביצוע אחד של המעגל ומדידה אחת. אם מריצים 1,024 shots, מקבלים 1,024 מחרוזות ביטים ומעריכים מהן הסתברויות. המספר אינו רק “יותר טוב”. יותר shots מצמצמים אי־ודאות דגימתית, אבל צורכים יותר זמן QPU, והם אינם מתקנים שגיאות חומרה שיטתיות.

QPU הוא המעבד הקוונטי הפיזי. Backend הוא המערכת שאליה Qiskit מכוון את העבודה: חומרה מסוימת, הטופולוגיה שלה, השערים שהיא תומכת בהם ונתוני הכיול שלה. Transpilation הוא התהליך שבו מעגל לוגי מתורגם לשערים ולחיבורים שהחומרה המסוימת באמת יודעת לבצע. מעגל קצר על הנייר עלול להפוך למעגל עמוק ורועש אם מיפוי הקיוביטים אינו מתאים.

Qiskit אינו מחשב קוונטי. זו ערכת פיתוח בקוד פתוח לבניית מעגלים, סימולציה, טרנספילציה והכנת workloads. שכבת Qiskit IBM Runtime מחברת את הקוד לשירותי IBM ול־QPUs שלה. בפרויקט הזה Qiskit שימש גם למתמטיקה המקומית וגם להכנת חבילת העבודה; רק קריאה אחת ל־sampler.run() עברה לחומרה.

מונחפירוש פשוטאצלנו
Qubitיחידת מידע קוונטיתשלושה קיוביטים פיזיים: 89, 91, 90
Circuitרצף פעולות ומדידותתבנית פרמטרית אחת ועוד 64 מעגלי כיול
Shotהרצה ומדידה אחת1,024 לכל תצורה
PUBיחידת קלט ל־Primitive של Qiskit88 יחידות עבודה
Backendיעד החומרה ותיאור היכולות שלוibm_kingston
QPU timeזמן שבו המעבד מוקצה לעבודה123 שניות, לא זמן ההמתנה בתור

IBM Quantum ועשר דקות QPU בחינם: מה באמת מקבלים

נכון למועד הפרסום, Open Plan של IBM מאפשר עד עשר דקות QPU בחלון מתגלגל של 28 יום. חשוב לדייק: זו אינה בהכרח “מתנה חדשה בכל ראשון לחודש”, ועשר הדקות אינן זמן קיר מרגע הלחיצה. IBM מודדת usage לפי הזמן שבו ה־QPU נעול עבור העבודה. זמן בתור, הורדה של תוצאות וניתוח מקומי אינם נספרים באותה צורה.

במקרה שלי ה־Job נוצר ב־23 ביולי 2026 בשעה 17:34:18 UTC, התחיל לרוץ רק ב־24 ביולי בשעה 01:49:54, והסתיים ב־01:52:36. כלומר המתנתי בערך שמונה שעות ורבע, אבל החיוב בפועל היה 123 שניות QPU — שתי דקות ושלוש שניות. זו דוגמה טובה להבדל בין זמן המתנה לזמן שימוש.

הגישה החינמית אינה מבטיחה שכל רעיון מתאים לחומרה. Job שבנוי בצורה לא יעילה יכול לשרוף חלק גדול מהמכסה, ופעולה שנכשלה או בוטלה עלולה לצרוך usage בנסיבות מסוימות. לכן בניתי תקרת ביצוע של 480 שניות, אומדן שמרני עם מרווח 50%, מנגנון ללא retry אוטומטי, ושער אישור מפורש לפני נקודת השליחה היחידה.

IBM פרסמה ב־2026 גם מסלול opt-in אפשרי של 180 דקות נוספות לחשבונות מתאימים לאחר צבירת שימוש מסוימת. זה מבצע בעל תנאים, לא תחליף למכסת הבסיס ולא הבטחה לכל חשבון. לפני הרצה אמיתית כדאי לבדוק את מסך התוכנית ואת התיעוד העדכני, משום שמכסות, זכאות וזמינות backend עשויות להשתנות.

בשורה אחת: עשר דקות QPU יכולות להספיק לניסוי רציני אם רוב העבודה — מתמטיקה, סימולציה, בדיקות, טרנספילציה וניתוח — מתבצעת מראש ובאופן מקומי.

שלב ההתקנה: Python, סביבה מבודדת וגרסאות שאפשר לשחזר

הסביבה הוקמה על Debian 13 עם Python 3.13.5. לא התקנתי חבילות לתוך Python המערכתי ולא הסתמכתי על “הגרסה האחרונה” באופן עיוור. יצרתי virtual environment נפרדת ונעלתי את ארבע החבילות שהיו חלק מהחישוב: Qiskit 2.5.0, ‏Qiskit IBM Runtime 0.48.0, ‏NumPy 2.5.1 ו־SciPy 1.18.0.

נעילת גרסאות אינה קישוט. ממשקי Runtime, מבני PUB, שמות options וסכמות serialization משתנים. קוד שעבד ביום הניסוי עלול להתנהג אחרת בעתיד אם מתקינים בלי pin. במאמר אני מציג את גרסאות המחקר בפועל; מי שמתחיל פרויקט חדש צריך לבדוק גם את התיעוד העדכני של IBM, אבל אם המטרה היא לשחזר את ההרצה הזאת — אלה הגרסאות הרלוונטיות.

הצגת הקוד: יצירת הסביבה והתקנת כל התלויות
terminal · bash
mkdir ibm-quantum-research
cd ibm-quantum-research

python3 -m venv .venv
source .venv/bin/activate

python -m pip install --upgrade pip
python -m pip install \
  qiskit==2.5.0 \
  qiskit-ibm-runtime==0.48.0 \
  numpy==2.5.1 \
  scipy==1.18.0

python -c "import qiskit, qiskit_ibm_runtime; print(qiskit.__version__, qiskit_ibm_runtime.__version__)"

שמרתי גם קובץ requirements.txt קצר. זה מאפשר להקים סביבה חדשה באותה פקודה, להפעיל בדיקות CI, ולתעד בדיוק מה היה מותקן בלי להסתמך על זיכרון.

הצגת הקוד: requirements.txt של המחקר
requirements.txt
qiskit==2.5.0
qiskit-ibm-runtime==0.48.0
numpy==2.5.1
scipy==1.18.0

אחרי ההתקנה לא קפצתי מיד ל־IBM. קודם בדקתי import, גרסאות וסימולציה מקומית. הפרדה זו חשובה מאוד: תקלה ב־Python, במעגל או במתמטיקה אינה סיבה לצרוך QPU. המחשב הקלאסי צריך לתפוס כל מה שהוא מסוגל לתפוס; החומרה הקוונטית נשמרת רק לשאלה שאי אפשר לענות עליה מקומית.

חיבור בטוח ל־IBM Quantum — והטעות הראשונה שעשיתי

החיבור ל־IBM דרש שני ערכים: הערך הסודי של API key, וה־CRN של מופע IBM Quantum. בניסיון הראשון הוזן מזהה או שם המפתח במקום הערך הסודי שבשדה apikey. התגובה הייתה ברורה: Provided API key could not be found. הוספת CRN אינה יכולה להציל מפתח IAM שגוי; קודם צריך לוודא שמעתיקים את הסוד עצמו ולא את שם המשאב, GUID או תווית שמתחילה ב־ApiKey-.

זו טעות שימושית לפרסום, מפני שהיא נפוצה וקלה לפספוס. היא גם ממחישה למה לא צריך להדביק secrets ישירות בקוד או בצ׳אט. בפרויקט האמיתי שמרתי את הסוד ואת ה־CRN בקבצים פרטיים מחוץ לתיקיית המחקר, עם הרשאות קריאה לבעלים בלבד. בגרסה הציבורית למטה הנתיבים כלליים ואין בה אף ערך אמיתי.

הצגת הקוד: שמירת credentials בלי להכניס סודות לקוד
terminal · bash
mkdir -p .secrets
chmod 700 .secrets

# ערכו את שני הקבצים מקומית. אל תדביקו את הערכים בקוד ציבורי.
${EDITOR:-nano} .secrets/ibm-api-key
${EDITOR:-nano} .secrets/ibm-instance-crn

chmod 600 .secrets/ibm-api-key .secrets/ibm-instance-crn

# ודאו שהתיקייה אינה נכנסת ל-Git:
printf ".secrets/\nqiskit-ibm.json\n" >> .gitignore
הצגת הקוד: יצירת שירות Runtime ובדיקת backends ללא Job
check_ibm_access.py · Python
from pathlib import Path
from qiskit_ibm_runtime import QiskitRuntimeService

TOKEN_FILE = Path(".secrets/ibm-api-key")
INSTANCE_FILE = Path(".secrets/ibm-instance-crn")

token = TOKEN_FILE.read_text(encoding="utf-8").strip()
instance = INSTANCE_FILE.read_text(encoding="utf-8").strip()

if not token or not instance:
    raise RuntimeError("Missing IBM Quantum credentials")

service = QiskitRuntimeService(
    channel="ibm_quantum_platform",
    token=token,
    instance=instance,
)

backends = service.backends(operational=True)
print("Operational backends:", len(backends))

# אין sampler.run() בקובץ הזה:
# הוא קורא metadata בלבד ואינו שולח עבודה לחומרה.

בדיקת ה־metadata מצאה שלושה backends פעילים בעלי 156 קיוביטים: ibm_fez, ‏ibm_marrakesh ו־ibm_kingston. בשלב הזה לא נשלח מעגל, לא נוצר Job וזמן ה־QPU נשאר אפס. זה הבדל שאני מקפיד לסמן גם בקוד: פונקציה שקוראת metadata אינה חולקת מסלול עם פונקציה שמסוגלת להפעיל חומרה.

בדיקה מקומית ראשונה: מצב Bell בלי IBM ובלי לבזבז מכסה

בדיקת smoke טובה צריכה להיות קצרה, חד־משמעית וללא תלות בשירות חיצוני. בניתי מעגל Bell בן שני קיוביטים: שער Hadamard יוצר סופרפוזיציה בקיוביט הראשון, ו־CNOT שוזר את השני. בסימולציה אידאלית התוצאות האפשריות הן רק 00 ו־11, כל אחת בהסתברות חצי.

הצגת הקוד: בדיקת Bell מקומית עם Statevector
verify_local.py · Python
from qiskit import QuantumCircuit
from qiskit.quantum_info import Statevector


def main() -> None:
    circuit = QuantumCircuit(2)
    circuit.h(0)
    circuit.cx(0, 1)

    probabilities = (
        Statevector
        .from_instruction(circuit)
        .probabilities_dict()
    )

    expected_states = {"00", "11"}
    if set(probabilities) != expected_states:
        raise RuntimeError(
            f"Unexpected Bell-state result: {probabilities}"
        )

    print("Local Bell-state test passed:", probabilities)


if __name__ == "__main__":
    main()

הבדיקה עברה והחזירה את ההתפלגות הצפויה. היא אינה מוכיחה שהחומרה של IBM זמינה, אבל היא מוכיחה ש־Python, ‏Qiskit והלוגיקה הבסיסית עובדים. בפרויקט מקצועי זו שכבה ראשונה בלבד: אחריה מגיעות בדיקות יחידה למתמטיקה, אימות של כל binding בסימולציית statevector, בדיקות מבניות של מניפסט, ורק אז preflight מול ה־backend.

לפני החומרה: ביקורת ספרות, ביקורת מתמטיקה והנכונות לתקן

המסמך התיאורטי המקורי עסק באלגברה על תתי־קבוצות, בהתאבכות מסדרים שונים ובאפשרות לתאר משקלים שאינם מוגבלים למבנה הרגיל של תורת הקוונטים. כאן היה קל מאוד ליפול למלכודת: לראות נוסחה שנראית חדשה, לתת לה שם ולהתקדם למחשב. במקום זאת בניתי audit נפרד של הספרות ו־audit נפרד של המתמטיקה.

בסקירת 27 מקורות התברר שחלק גדול מהליבה האלגברית קשור לכלים מוכרים: zeta transform, היפוך Möbius, מכפלת covering/union convolution, הפרשים סופיים ופולינומים pseudo-Boolean. לכן לא היה בסיס לטענה רחבה של “אלגברה חדשה”. הניסוח תוקן: אפשר להציע סינתזה או פרשנות פיזיקלית מסוימת, אבל אסור להציג כלים מתמטיים מוכרים כחידוש.

גם בביקורת המתמטית נמצאו נקודות שחייבו תיקון. בניסוח החיוביות היה חסר שורש ריבועי אופרטורי: הביטוי הנכון משתמש ב־EA(ρ)1/2. הטענה שדינמיקה כללית שומרת אוטומטית על דרגת הפולינום הייתה רחבה מדי; צריך להניח זאת או להוכיח עבור מחלקת דינמיקות מוגדרת. cutoff על משקלים לא מנורמלים אינו עובר אוטומטית להסתברויות שמנורמלות מחדש בכל context. והפרש מעורב משחזר coefficient יחיד רק בבסיס מתאים, לא בכל נקודת בסיס.

הפרטים האלה אינם הערות שוליים. אם ההגדרה המתמטית אינה מדויקת, המעגל עלול לבדוק דבר אחר ממה שהמאמר טוען. אם prior art אינו ממופה, “תוצאה חדשה” עלולה להיות ניסוח מחדש של משהו ותיק. לכן הטיוטה המתוקנת סימנה כל רכיב בתווית: הגדרה, מתמטיקה ידועה, למה, השערה או בעיה פתוחה. היא גם הבהירה שהשוויון הגורף “HQC מדרגה 1 = כל תורת הקוונטים” אינו מוצדק.

זו בעיניי אחת היכולות החשובות ביותר שהפרויקט מדגים: לא רק לכתוב קוד, אלא לבקר את ההנחות לפני שהקוד הופך אותן לעובדה לכאורה. אותו עיקרון עומד מאחורי העבודה שלי על הפיכת רעיון וקוד למערכת שאפשר לבדוק ולשחזר: תיעוד, גבולות טענה ובדיקות הם חלק מהארכיטקטורה, לא שלב קוסמטי בסוף.

מה באמת ביקשנו לבדוק: Sorkin null benchmark

לאחר הביקורת צמצמתי את מטרת החומרה לשאלה ש־QPU רגיל של IBM יכול לענות עליה. לקחתי ניסוי מחשבתי של ארבעה מסלולים. לכל context פותחים תת־קבוצה אחרת של המסלולים, מודדים את ההסתברות לאירוע קבוע, ומחברים את ההסתברויות עם סימנים מתחלפים. בתורת הקוונטים הסטנדרטית אמורות התאבכויות מסדר שלישי ורביעי להתאפס במודל האידאלי.

In(S) = ΣT⊆S (−1)|S|−|T| P(T)

עבור כל שלישייה מתוך ארבעת המסלולים חישבתי I3, ועבור כל הארבעה חישבתי I4. אם כל הערכים קרובים לאפס ביחס לאי־הוודאות ולמעטפת השיטתית, הצינור מתנהג כפי שמצופה מ־QM רגילה. אם מתקבלת סטייה, המסקנה הראשונה אינה “פיזיקה חדשה”. קודם בודקים רעש קריאה, drift, עומק מעגל, gate errors, מיפוי, נרמול, post-selection, ורק לאחר רפליקציה ופסילת הסברים רגילים אפשר לדבר על anomaly מועמד.

הפרוטוקול שמר על מקור קבוע: שני קיוביטים יצרו אמפליטודה אחידה של חצי על ארבעה מצבי מסלול. קיוביט שלישי שימש דגל loss/pass. context שינה אילו מסלולים עוברים, אבל המדידה נשארה קבועה. האירוע שנבדק היה 000. לא ביצעתי post-selection על “שורדים” ולא נרמלתי כל context בנפרד, מפני ששתי הפעולות האלה עלולות לשנות את אלגברת הקונטרסט וליצור אפקט מלאכותי.

נוספו שלושה סוגי בלוקים: 16 בלוקים ראשיים, ארבע בקרות סימן וארבע בקרות פאזה. כל בלוק כלל את כל 16 ה־contexts. סדר ה־contexts בתוך בלוק וסדר הבלוקים רונדמו בצורה דטרמיניסטית עם seed קבוע, ונעשה מיפוי Latin-square בין מסלולים לייצוגים פיזיים. כך אפשר לבדוק אם האפקט תלוי בתווית, בפאזה או במחצית ההרצה.

התכנון הראשון נפסל לפני QPU — וזה היה הרגע החשוב ביותר

בגרסה הראשונה נבנה מעגל שונה לכל context. פונקציונלית, כל מעגל מימש את מסכת המסלולים המתאימה. אבל לאחר transpilation התברר שהמעגלים אינם שווי־עלות: העומק נע בין 1 ל־37, ומספר שערי CZ נע בין 0 ל־10. כלומר context עם יותר מסלולים סגורים קיבל לעיתים מעגל עמוק ורועש יותר.

זו בעיית confounding קלאסית. נוסחת I4 מחברת ומחסרת הסתברויות של contexts שונים. אם שיעור השגיאה עצמו מתואם עם גודל ה־context, השילוב יכול להיראות כמו התאבכות מסדר רביעי גם כאשר הפיזיקה רגילה לחלוטין. במודל צעצוע של הרעש המבני התקבל I4≈0.022, גדול מאי־הוודאות הדגימתית המשוערת של כ־0.0085. במילים פשוטות: התכנון היה מסוגל לייצר “תגלית” מהמנגנון שלו.

לא הרצתי אותו. שמרתי את ה־QPY ואת תוכנית ה־dry-run תחת תווית REJECTED, תיעדתי את הסיבה ובניתי מחדש. השמירה חשובה: היא מראה שלא שיניתי את הסיפור בדיעבד לאחר שראיתי תוצאה, אלא זיהיתי את הכשל לפני חומרה. גם בעולם העסקי זהו הרגל בריא — לא למחוק ניסוי לא טוב כאילו לא היה, אלא לתעד מה למדנו ממנו.

למה הפסילה חשובה יותר מתוצאה יפה:
אם מדד המחקר מתואם עם כמות הרעש שהקוד עצמו יוצר, אי אפשר לדעת אם מודדים את התופעה או את המימוש. תכנון אחיד היה תנאי סף, לא שיפור אופציונלי.

התכנון המתוקן: מעגל פרמטרי אחד לכל 16 ההקשרים

הפתרון היה לבנות template יחיד עם אותה טופולוגיה ואותם סוגי שערים בכל context. ארבעה פרמטרי theta קובעים אילו מסלולים עוברים לדגל ה־loss, ושני פרמטרי phase מאפשרים בקרות פאזה. המעגל נטרנספל פעם אחת; אחר כך נשלחו רק מטריצות ערכים שונות. כך context אינו מקבל “יתרון” של מעגל קצר יותר.

Qiskit אינו מקבל כאן ישירות uniformly controlled rotation סימבולי בכל צורה, ולכן השתמשתי בפירוק Shende/Möttönen קטן שמחשב את זוויות ה־Ry הסימבוליות. זהו הקוד האמיתי הדרוש להפעלת התבנית, לאחר הסרת metadata מילולי בלבד.

הצגת הקוד המלא: התבנית הפרמטרית האחידה
hqc_prepare_sorkin_experiment.py · Python
from typing import Any

from qiskit import QuantumCircuit
from qiskit.circuit import ParameterVector


def decompose_uniform_rotation_angles(
    angles: list[Any],
    start: int,
    end: int,
    reverse: bool,
) -> None:
    half = (end - start) // 2

    for index in range(start, start + half):
        first = angles[index]
        second = angles[index + half]

        if not reverse:
            angles[index] = (first + second) / 2
            angles[index + half] = (first - second) / 2
        else:
            angles[index + half] = (first + second) / 2
            angles[index] = (first - second) / 2

    if half > 1:
        decompose_uniform_rotation_angles(
            angles, start, start + half, False
        )
        decompose_uniform_rotation_angles(
            angles, start + half, end, True
        )


def parametric_context_template():
    theta = ParameterVector("theta", 4)
    phase = ParameterVector("phase", 2)

    beta: list[Any] = list(theta)
    decompose_uniform_rotation_angles(
        beta, 0, len(beta), False
    )

    qc = QuantumCircuit(
        3, 3, name="sorkin_four_path_parametric"
    )

    # Uniform amplitude 1/2 over four path states.
    qc.h(0)
    qc.h(1)

    # Same topology for primary and control bindings.
    qc.p(phase[0], 0)
    qc.p(phase[1], 1)

    controls = (0, 1)
    for index, angle in enumerate(beta):
        qc.ry(angle, 2)

        if index != len(beta) - 1:
            value = index + 1
            control_index = (
                (value & -value).bit_length() - 1
            )
        else:
            control_index = len(controls) - 1

        qc.cx(controls[control_index], 2)

    qc.h(0)
    qc.h(1)
    qc.measure((0, 1, 2), (0, 1, 2))

    return qc, theta, phase

לכל מסלול פתוח הוצמד ערך 0, ולמסלול חסום זווית π. עבור בקרת סימן הוחלפה זווית החסימה, ועבור בקרת פאזה הוזנו תבניות פאזה אדיטיביות. לפני ה־QPU כל 384 ה־bindings הוצמדו ל־template ונבדקו באמצעות Statevector. כל 24 הבלוקים הכילו את כל 16 המסכות, וכל ערכי I3 ו־I4 האידאליים התאפסו עד שגיאת נקודה צפה.

בסך הכול נשמרו ב־QPY רק 65 circuit objects: תבנית ניסוי אחת ו־64 מעגלי readout calibration. המספר 448 אינו מספר מעגלים ייחודיים; הוא 384 תצורות פרמטרים ניסויות ועוד 64 כיולים. הדיוק הזה חשוב, מפני שהכותרת “448 מעגלים” הייתה נשמעת מרשימה יותר — אבל פשוט אינה נכונה.

בחירת ibm_kingston, שלושה קיוביטים ו־transpilation

שלושת ה־backends שנבדקו הציעו 156 קיוביטים, אבל הניסוי דרש שני קיוביטי מסלול וקיוביט דגל אחד. לא ניסיתי “למלא” את המעבד כדי לייצר מספר גדול בכותרת. חיפשתי שלישייה מחוברת בטופולוגיית ה־heavy-hex, עם שגיאות שער דו־קיוביטי ו־readout נמוכות, זמני קוהרנטיות סבירים ומעגל שניתן למפות בלי SWAP.

המיפוי שנבחר היה [89, 91, 90] עבור q0, q1, flag על ibm_kingston. ה־backend נבחר לפי snapshot כיול בזמן ה־preflight, לא לפי שם מוכר ולא רק לפי אורך התור. כיול הוא מידע זמני: מערכת טובה בשעה אחת יכולה להשתנות, ולכן אסור להעתיק layout ממאמר ולהניח שהוא אופטימלי ביום אחר.

ה־transpiler קיבל את ה־target של החומרה, optimization level 3, layout התחלתי ו־seed קבוע. התוצאה הסופית הייתה depth 26, ארבעה שערי CZ, ‏19 פעולות RZ, ‏13 פעולות SX, שלוש מדידות וללא SWAP. משך התבנית המוערך היה כ־2.74 מיקרו־שניות. הנתונים האלה משמעותיים יותר ממספר הקיוביטים הכולל, משום שהם מתארים את העומס האמיתי על שלושת הקיוביטים שנבחרו.

הצגת הקוד: transpilation ואומדן שמרני של זמן QPU
preflight · Python
from qiskit.transpiler.preset_passmanagers import (
    generate_preset_pass_manager,
)

SHOTS = 1_024
SEED = 20_260_723

pass_manager = generate_preset_pass_manager(
    target=backend.target,
    optimization_level=3,
    initial_layout=[89, 91, 90],
    seed_transpiler=SEED,
)

transpiled_template = pass_manager.run(template)
transpiled_calibrations = pass_manager.run(calibrations)

template_duration = float(
    transpiled_template.estimate_duration(
        backend.target, unit="s"
    )
)

effective_circuits = (
    len(bindings) + len(transpiled_calibrations)
)
executions = effective_circuits * SHOTS

metadata_estimate = (
    2.0
    + (
        len(bindings)
        * (backend.default_rep_delay + template_duration)
        + sum(
            backend.default_rep_delay
            + float(
                circuit.estimate_duration(
                    backend.target, unit="s"
                )
            )
            for circuit in transpiled_calibrations
        )
    )
    * SHOTS
)

ibm_quick_estimate = 2.0 + 0.00035 * executions
conservative_estimate = 1.5 * max(
    metadata_estimate,
    ibm_quick_estimate,
)

if conservative_estimate > 480:
    raise RuntimeError(
        "NO-GO: estimated execution exceeds hard cap"
    )

Preflight, שערי איכות ואישור אנושי לפני הוצאה

קובץ ההכנה עצמו לא ייבא Sampler, ‏Estimator, ‏Session או Batch ולא הכיל אף קריאת run. הוא היה מסוגל לקרוא metadata, לבצע Statevector, לטרנספל, לחשב אומדנים ולכתוב artifacts — אבל לא לצרוך QPU. זו הפרדת הרשאות ברמת הקוד: גם אם מפעילים את קובץ ההכנה בטעות, הוא לא יכול לשלוח עבודה.

ה־runner המלא התחיל כברירת מחדל בבדיקה מקומית. subcommand נפרד ביצע metadata preflight, ורק subcommand שלישי היה מסוגל לשלוח. לפני השליחה נבדקו operational status, גיל הכיול, שגיאת CZ מרבית מתחת ל־0.015, שגיאת readout מתחת ל־0.03, אפס SWAP, לא יותר משישה שערים שזורים, ומשך מעגל הנמוך מעשרה אחוזים מזמן הקוהרנטיות המינימלי שנמדד.

כל חבילת הניסוי הוקפאה: קובץ QPY, תוכנית bindings, analyzer, מסמך התכנון, הטיוטה המתוקנת ו־requirements. לכל קובץ חושב SHA-256, והמניפסט קשר את ה־hashes לסדר ה־PUBs ול־snapshot החומרה. ה־approval capsule היה תקף ל־20 דקות בלבד. השליחה דרשה hash אישור מדויק, משפט אישור מפורש ו־lock אטומי שמנע retry אוטומטי או Job כפול.

ב־preflight הסופי נמצאו 22 Jobs בתור. אומדן ה־metadata היה 117.945 שניות; נוסחת האומדן המהירה נתנה 162.563 שניות; לאחר מרווח ביטחון של 50% נקבע אומדן שמרני של 243.845 שניות. כל הערכים היו מתחת לתקרה של 480 שניות. האומדנים אינם הבטחת חיוב, אבל הם הפכו את ההחלטה למדידה ולא להימור.

לפני האישור עברו 56 מתוך 56 בדיקות. הן לא הסתפקו בשאלה אם פונקציה “רצה”: הן בדקו את הגדרות הקונטרסטים, שלמות כל 16 המסכות בכל בלוק, סדר ואינדקסים של 88 ה־PUBs, צורת מטריצת ה־bindings, מספר ה־shots, digest של החבילה, חסימת שליחה ללא capsule תקף, ואיסור retry. נוסף לכך נבנה payload סינתטי מלא בן 448 records. ה־analyzer קיבל אותו, שחזר את תחזית האפס וסיווג אותו כ־null validation כאשר כל תנאי היושרה הסינתטיים היו זמינים. כך נבדק צינור הניתוח לפני שהוא ראה את נתוני IBM האמיתיים.

בדיקות אינן מבטיחות שאין באגים; הן מצמצמות את מרחב הכשל ומבהירות מה הובטח מראש. במקרה הזה הן גם עזרו להפריד בין שתי שכבות: הנתונים הניסויים והסטטיסטיקה עברו, אבל הנחת התזמון לא הייתה מכוסה משום שרק הפלט החי חשף את רמת הגרנולריות של ExecutionSpans. זה בדיוק סוג הפער שביקורת לאחר הרצה צריכה לתעד.

הצגת הקוד: שערי האיכות שמכריעים GO או NO-GO
quality_gates · Python
quality_gates = {
    "operational": bool(status.operational),
    "calibration_younger_than_4h": (
        0.0 <= age_seconds < 4 * 3600
    ),
    "max_cz_error_below_0_015": (
        max(row["error"] for row in cz_rows) < 0.015
    ),
    "max_readout_error_below_0_03": (
        max(row["error"] for row in readout_rows) < 0.03
    ),
    "no_swap": operation_counts.get("swap", 0) == 0,
    "no_more_than_6_entanglers": (
        operation_counts.get("cz", 0) <= 6
    ),
    "circuit_below_10pct_min_t1_t2": (
        duration < 0.1 * min(*t1, *t2)
    ),
}

failed = [
    name
    for name, passed in quality_gates.items()
    if not passed
]

if failed:
    raise RuntimeError(f"NO-GO: {failed}")

איך 88 PUBs הפכו ל־448 תצורות — ונשלחו ב־Job יחיד

PUB הוא קיצור של Primitive Unified Bloc: יחידת קלט ל־Primitive של Qiskit. ב־Sampler PUB יכול להכיל מעגל, מערך ערכי פרמטרים ומספר shots. זה מאפשר לשלוח template אחד עם bindings וקטוריים במקום לשכפל מעגל כמעט זהה מאות פעמים.

הלו״ז הלוגי שלנו היה 32 PUBs של readout calibration “לפני”, אחריהם 24 PUBs ניסויים שכל אחד מהם הכיל 16 bindings, ואז 32 PUBs של כיול “אחרי”. החשבון הוא 32 + 24 + 32 = 88 PUBs, אבל מספר התצורות הוא 32 + ‏(24×16) + 32 = 448. כל תצורה קיבלה 1,024 shots, ולכן נוצרו 458,752 תוצאות מדידה.

הכיולים כיסו את כל שמונת מצבי שלושת הקיוביטים, ארבע חזרות בכל צד: 8×4×2 = 64. מהם נבנו מטריצות assignment בגודל 8×8, אחת עבור “לפני” ואחת עבור “אחרי”. כך אפשר היה לנתח raw ולצדן שתי גרסאות mitigation שונות, ולבדוק אם המסקנה תלויה בבחירת כיול הקריאה.

הצגת הקוד: בניית סדר של 88 PUBs ואימות 448 תצורות
build_pub_specs · Python
from collections import OrderedDict


def build_pub_specs(rows, calibrations):
    specs = []

    def add_calibrations(position):
        for repeat in range(4):
            for state in range(8):
                circuit_index, _ = calibrations[
                    (position, repeat, state)
                ]
                specs.append(
                    PubSpec(
                        pub_index=len(specs),
                        kind="readout_calibration",
                        circuit_index=circuit_index,
                        effective_configurations=1,
                        calibration={
                            "position": position,
                            "repeat": repeat,
                            "prepared_state": state,
                        },
                    )
                )

    add_calibrations("before")

    blocks = OrderedDict()
    for row in rows:
        blocks.setdefault(
            str(row["block_id"]), []
        ).append(row)

    for block_id, block_rows in blocks.items():
        ordered_rows = tuple(
            sorted(
                block_rows,
                key=lambda row: int(
                    row["order_in_block"]
                ),
            )
        )
        specs.append(
            PubSpec(
                pub_index=len(specs),
                kind="experiment",
                circuit_index=0,
                effective_configurations=16,
                block_id=block_id,
                experiment_type=str(
                    ordered_rows[0]["experiment_type"]
                ),
                binding_rows=ordered_rows,
            )
        )

    add_calibrations("after")

    if len(specs) != 88:
        raise AssertionError(
            f"Expected 88 PUBs, got {len(specs)}"
        )

    configurations = sum(
        spec.effective_configurations
        for spec in specs
    )
    if configurations != 448:
        raise AssertionError(
            "Expected 448 configurations"
        )

    return specs

ה־Sampler PUBs הניסויים נבנו עם BindingsArray בצורת 16 שורות על שישה פרמטרים. לכל שורה נשמרו block ID, סוג ניסוי, מסכה לוגית, mapping, סדר בתוך הבלוק ואינדקס binding במניפסט המקומי. ה־metadata המפורט הזה לא כולו הועבר ל־IBM; הוא נשמר מקומית ונקשר בתוכן מוצפן־hash לתוצאות. זה מאפשר למפות כל count חזרה לכוונה המדעית, בלי להסתמך על סדר ידני.

הצגת הקוד: BindingsArray ו־SamplerPub עבור כל בלוק
build_sampler_pubs · Python
import numpy as np
from qiskit.primitives.containers import (
    BindingsArray,
    SamplerPub,
)

EXPECTED_SHOTS = 1_024


def build_sampler_pubs(
    template,
    calibrations,
    specs,
):
    by_circuit_index = {
        circuit_index: circuit
        for circuit_index, circuit
        in calibrations.values()
    }

    parameter_by_name = {
        str(parameter): parameter
        for parameter in template.parameters
    }
    ordered_parameters = tuple(
        parameter_by_name[name]
        for name in sorted(parameter_by_name)
    )

    pubs = []
    for spec in specs:
        if spec.kind == "readout_calibration":
            circuit = by_circuit_index[
                spec.circuit_index
            ]
            pubs.append(
                SamplerPub(
                    circuit,
                    shots=EXPECTED_SHOTS,
                )
            )
            continue

        matrix = np.asarray(
            [
                [
                    row_parameter_map(row)[
                        str(parameter)
                    ]
                    for parameter in ordered_parameters
                ]
                for row in spec.binding_rows
            ],
            dtype=float,
        )

        bindings = BindingsArray({
            ordered_parameters: matrix
        })
        pub = SamplerPub(
            template,
            parameter_values=bindings,
            shots=EXPECTED_SHOTS,
        )

        if pub.parameter_values.shape != (16,):
            raise ValueError(
                "Unexpected binding shape: "
                f"{pub.parameter_values.shape}"
            )

        pubs.append(pub)

    return pubs

לאחר אישור מפורש בוצעה נקודת החומרה היחידה. dynamical decoupling ו־twirling הושבתו בכוונה כדי לשמור הגדרה תפעולית אחידה ופשוטה לניתוח. max_execution_time הוגדר ל־480 שניות. הקטע הבא הוא גרסה ציבורית מקוצרת; ה־runner האמיתי הוסיף אימותי hash, capsule, נעילה אטומית ושמירת Job ID לפני כתיבת receipt.

הצגת הקוד: הקריאה היחידה שמפעילה חומרה ועלולה לצרוך QPU
SamplerV2 · Python
from qiskit_ibm_runtime import (
    SamplerOptions,
    SamplerV2,
)

# Chargeable hardware call:
# run only after local checks, metadata preflight
# and explicit human approval.
options = SamplerOptions(
    max_execution_time=480,
    default_shots=1_024,
    execution={
        "init_qubits": True,
        "meas_type": "classified",
    },
    dynamical_decoupling={
        "enable": False,
    },
    twirling={
        "enable_gates": False,
        "enable_measure": False,
    },
)

sampler = SamplerV2(
    mode=backend,
    options=options,
)

job = sampler.run(pubs)
print("Submitted job:", "<IBM_JOB_ID>")

ה־Job נשלח פעם אחת, ללא retry. IBM החזירה סטטוס complete. נאספו בדיוק 88 תוצאות PUB, אינדקסים 0–87; ‏448 records, אינדקסים 0–447; וכל record הכיל בדיוק 1,024 counts. קבלת הייצוא כללה SHA-256 של raw counts ושל PrimitiveResult, כך שאפשר לזהות שינוי בקבצים לאחר האיסוף.

איסוף Job קיים הוא מסלול קריאה בלבד; הוא אינו מפעיל sampler.run() ואינו יוצר עבודה נוספת. הקוד בדק שבכל PUB יש BitArray יחיד, שכל מפתח count הוא מחרוזת של שלושה ביטים, שכל count הוא מספר שלם לא־שלילי, ושהסכום בכל תצורה הוא בדיוק 1,024. רק לאחר שכל הבדיקות עברו נכתב קובץ raw והופקה קבלת ייצוא.

הצגת הקוד: איסוף Job קיים ואימות ה־counts
collect_existing_job · Python
job = service.job("<IBM_JOB_ID>")
result = job.result()

if len(result) != 88:
    raise ValueError(
        f"Expected 88 PUB results, got {len(result)}"
    )


def counts_for(bit_array, location=None):
    counts = bit_array.get_counts(loc=location)
    normalized = {}

    for bitstring, count in counts.items():
        bits = str(bitstring).replace(" ", "")

        if len(bits) != 3 or any(
            bit not in "01" for bit in bits
        ):
            raise ValueError(
                f"Invalid count key: {bitstring!r}"
            )

        if not isinstance(count, int) or count < 0:
            raise ValueError(
                f"Invalid count: {count!r}"
            )

        normalized[bits] = (
            normalized.get(bits, 0) + count
        )

    if sum(normalized.values()) != 1_024:
        raise ValueError(
            "Counts do not sum to 1,024 shots"
        )

    return dict(sorted(normalized.items()))

חשוב כבר כאן להבדיל בין סדר הרשימה לבין סדר הביצוע הפיזי. הקוד בנה לפני→ניסוי→אחרי, והתוצאות חזרו ממופות לפי PUB index. אבל IBM אינה מבטיחה שהמעגלים מ־PUBs מבוצעים באותו סדר. ההבדל הזה הוא לב המגבלה שאציג בהמשך.

איך ניתחתי את התוצאות בלי “לצוד” מובהקות

הניתוח הוגדר לפני הריצה ונבדק על נתונים סינתטיים. זה חשוב משום שקל לשנות בדיעבד את המדד, את החיתוך או את גרסת המיטיגציה עד שאחד המספרים נראה מעניין. כאן הוגדרו מראש חמישה קונטרסטים ראשיים: ארבעה ערכי I3 — אחד לכל שלישיית מסלולים — ו־I4 אחד לכל ארבעת המסלולים.

כיול readout: תיקון אפשרי, לא מחיקת רעש קסומה

מדידת מצב קיוביט יכולה לטעות: 0 נקרא כ־1 או להפך. 64 מעגלי הכיול אפשרו לבנות מטריצת assignment בגודל 8×8 לפני ואחרי הניסוי. condition number היה 1.033315 לפני ו־1.032320 אחרי; השינוי המרבי בין המטריצות היה 0.003662, מתחת לסף 0.02. כל חזרות ה־Bootstrap המתוקנות עברו את סף היציבות.

ובכל זאת, מיטיגציה אינה “אמת מתוקנת”. היא מודל שמנסה להפוך שגיאות קריאה לפי כיול. לכן שמרתי שלוש תוצאות מקבילות: raw, ‏mitigated-before ו־mitigated-after. אם המסקנה משתנה בין השלוש, זו אינדיקציה לרגישות מערכתית — לא סיבה לבחור את הגרסה שנראית הכי יפה.

Bootstrap היררכי: לדגום את המבנה, לא רק את השורות

הניתוח הראשי כלל 20,000 חזרות Bootstrap עם seed קבוע. בכל חזרה נדגמו בלוקים עם החזרה, בוצעה דגימה מולטינומית של ה־counts בתוך כל context, ונדגמו מחדש חזרות הכיול. אותן דגימות שימשו את raw ושתי גרסאות המיטיגציה כדי לאפשר השוואה מזווגת. אם condition number של מטריצת כיול בחזרה מסוימת היה גדול מ־10, גרסת המיטיגציה באותה חזרה נפסלה.

לאחר מכן בוצעה ביקורת עצמאית נפרדת עם 200,000 חזרות ו־seed אחר. היא שחזרה רווחי סמך כמעט זהים: כל חמשת רווחי הסמך כללו אפס, ואף מבחן לא נשאר מובהק לאחר תיקון לריבוי השוואות. חשוב לדייק: קובץ הניתוח הרשמי מתעד את 20,000 החזרות; 200,000 היו audit נוסף, לא שינוי של הפלט המקורי.

תיקון Holm: חמש שאלות דורשות רף זהיר יותר

כאשר בודקים חמש השערות, הסיכוי שלפחות אחת תיראה חריגה במקרה גבוה יותר מאשר בבדיקה אחת. תיקון Holm מסדר את ערכי ה־p ומתקן אותם בהדרגה, בלי להיות שמרני כמו Bonferroni בכל מצב. הוגדרו גם רף 0.05 וגם רף חמור של 5σ עבור anomaly מועמד. אף קונטרסט לא עבר אף אחד מהרפים.

הצגת הקוד: חישוב I3/I4 ותיקון Holm
hqc_analyze_counts.py · Python
def contrast_terms(paths):
    terms = {}
    n = len(paths)

    for subset_bits in range(1 << n):
        mask = 0
        size = 0

        for offset, path in enumerate(paths):
            if subset_bits & (1 << offset):
                mask |= 1 << path
                size += 1

        terms[mask] = (-1) ** (n - size)

    return terms


CONTRAST_TERMS = {
    "I3_012": contrast_terms((0, 1, 2)),
    "I3_013": contrast_terms((0, 1, 3)),
    "I3_023": contrast_terms((0, 2, 3)),
    "I3_123": contrast_terms((1, 2, 3)),
    "I4_0123": contrast_terms((0, 1, 2, 3)),
}


def contrasts(probabilities):
    if set(probabilities) != set(range(16)):
        raise ValueError(
            "A complete block must contain all 16 contexts"
        )

    return {
        name: float(
            sum(
                sign * probabilities[mask]
                for mask, sign in terms.items()
            )
        )
        for name, terms in CONTRAST_TERMS.items()
    }


def holm_adjust(p_values):
    ordered = sorted(
        p_values,
        key=lambda name: (p_values[name], name),
    )

    adjusted = {}
    running = 0.0
    total = len(ordered)

    for rank, name in enumerate(ordered):
        candidate = min(
            1.0,
            (total - rank) * float(p_values[name]),
        )
        running = max(running, candidate)
        adjusted[name] = running

    return adjusted
הצגת הפקודה: הפעלת הניתוח הקפוא עם 20,000 חזרות
terminal · bash
python hqc_analyze_counts.py \
  --input "<RAW_COUNTS.json>" \
  --output "<ANALYSIS.json>" \
  --bootstrap-reps 20000 \
  --seed 20260723

התוצאות הגולמיות

הטבלה מציגה את אומדן ה־raw, רווח הסמך של 95% מ־Bootstrap וערך ה־p לאחר Holm. ערכים מעוגלים כאן לחמש ספרות; קובצי המחקר שומרים את הדיוק המלא.

קונטרסטאומדן rawרווח סמך 95%Holm pמסקנה
I3_012−0.01819[−0.03961, 0.00311]0.4735לא מובהק
I3_013−0.00232[−0.02289, 0.01855]0.8256לא מובהק
I3_023−0.01166[−0.03119, 0.00793]0.7340לא מובהק
I3_123−0.00989[−0.03089, 0.01111]0.7340לא מובהק
I4_01230.02417[−0.00562, 0.05548]0.4774לא מובהק

כל חמשת רווחי הסמך כוללים אפס. גם התוצאות המתוקנות בכיול “לפני” ובכיול “אחרי” היו קרובות מאוד מספרית, ואף אחת לא דחתה את השערת האפס. עם זאת, ההשוואה המזווגת סימנה שלושה הבדלים קטנים בין raw למיטיגציה — I3_012 מול שתי המטריצות ו־I4_0123 מול כיול “לפני” — ולכן הם נכנסו כדגלי רגישות לסיווג.

מעטפת השיטתיות B_sys שילבה הבדלי early/late, בקרות סימן, בקרות פאזה והפרש בין שתי מטריצות הקריאה. עבור I4 היא הייתה כ־0.02546, דומה בגודלה לאומדן 0.02417. זו עוד סיבה לא לפרש את המספר החיובי לבדו כראיה לחריגה.

פסק הדין שנרשם

אז הבעיה הייתה אצלנו או אצל IBM? התשובה המדויקת

לא הייתה שליחה חסרה או פגומה. בדיקה מול קלט ה־Job מצאה התאמה לכל 88 המעגלים, מספרי ה־shots ומטריצות הפרמטרים. IBM החזירה את כל 88 תוצאות ה־PUBs, את כל 448 התצורות ואת כל 458,752 המדידות. אינדקסי ה־PUB, ה־bindings וה־records היו רציפים וללא חוסרים.

מה שחסר הוא ראיית תזמון גרנולרית. IBM החזירה ExecutionSpan יחיד לכל 88 ה־PUBs, מהשעה 01:50:22.678250 עד 01:52:23.662394 UTC. חלון כזה אומר שהנתונים נאספו בתוכו; הוא אינו חותמת זמן נפרדת והדוקה לכל PUB. בפועל כל 448 הרשומות קיבלו את אותה נקודת executed_at שנגזרה מתחילת החלון.

הכוונה הלוגית שלנו הייתה ברורה: כיול לפני, ניסוי, כיול אחרי. אבל תיעוד IBM אומר שאין הבטחה שסדר המעגלים מ־PUBs הוא סדר הביצוע, ואף אין הבטחה שכל המעגלים של Job ירוצו כ־batch פיזי יחיד. לכן PUB index, סדר הרשימה ו־execution_index מקומי אינם הוכחה שהחומרה ביצעה את שלושת החלקים כרונולוגית כפי שתכננו.

זו מגבלה משולבת. מצד IBM, ה־API אינו מספק timestamp מוחלט נפרד לכל PUB ואינו מתחייב לסדר. מצדנו, הנחת התכנון הייתה ש־ExecutionSpans יספיקו להוכיח bracketing. ההנחה הזאת הייתה שגויה. המסקנה אינה ש־IBM “איבדה נתונים”, אלא שהפרוטוקול ביקש הוכחת סדר מפלטפורמה שאינה מבטיחה אותה.

אירועאחריותהשפעה בפועל
הוזן שם/מזהה API key במקום הסודאצלנוחסם התחברות ראשונית; אפס QPU
88 PUBs ו־448 תצורותעברו תקיןלא חסר מידע ניסויי
חלון execution מצטברמגבלת IBM + הנחת תכנון שלנואין הוכחת before→experiment→after
NaN בשיפועי זמןedge case ב־analyzer שלנולא שינה אומדנים; פגע בשחזור JSON byte-for-byte

תקלה מקומית נפרדת: 15 ערכי NaN שהומרו ל־null

כאשר כל ה־PUBs קיבלו אותו חלון זמן, מערך הזמנים המנורמל היה קבוע. קריאת linregress על ציר שאין בו שונות החזירה NaN עבור slope, ‏p-value ו־predicted change — 15 שדות בסך הכול. כתיבת JSON הוגדרה בכוונה עם allow_nan=False, ולכן היא נעצרה במקום להפיק JSON לא תקני. השדות הומרו ל־null בייצוא הסופי.

הטיפול הזה לא שינה counts, אומדני I3/I4, Bootstrap, ‏Holm, מעטפת שיטתית או classification. אבל הוא אומר שה־CLI המוקפא אינו משחזר כרגע את קובץ הניתוח byte-for-byte בלי תיקון ה־edge case. זו מגבלת reproducibility אמיתית, ולכן היא שייכת למאמר ולא להערת שוליים נסתרת.

הצגת הקוד: התיקון שיוחל לפני הרצה נוספת
time-axis guard · Python
span = raw_time.max() - raw_time.min()

if span == 0:
    time_available = False
    times = None
else:
    time_available = True
    times = (
        raw_time - raw_time.min()
    ) / span

הקוד הזה הוא תיקון שלא היה בחבילה המוקפאת לפני ההרצה; אני מציג אותו כפעולת שיפור לעתיד, לא כאילו היה חלק מהפרוטוקול המקורי.

מה אפשר לשחזר עכשיו, ומה אשנה בניסוי הבא

גם ללא חשבון IBM אפשר לשחזר חלק גדול מהעבודה: להקים את הסביבה, להריץ את Bell test, לבנות את התבנית הפרמטרית, להצמיד את כל 384 ה־bindings, לאמת את 24 הבלוקים ב־Statevector, להריץ 56 בדיקות יחידה ולהפעיל את pipeline הסינתטי עם 448 records. רק שלב החומרה דורש חשבון, instance פעיל ומכסת QPU.

לפני כל הרצה נוספת הייתי משנה שלושה דברים. ראשית, מקפיא מראש מדיניות מפורשת ל־missing או aggregate timing, כולל דילוג על רגרסיית זמן כאשר אין שונות. שנית, לא משתמש ב־PUB index כתחליף לשעון. שלישית, מתכנן פרוטוקול שאינו תלוי בסדר פיזי פנימי, או מפצל אותו לשלבים עוקבים שניתן לאמת את השלמתם בנפרד — רק לאחר בדיקה שהתוכנית וה־API אכן מתחייבים למידע הדרוש ושעלות השלבים נשארת בתוך התקציב.

אפשרות חזקה יותר היא תכנון self-calibrating: לשזור בקרות בתוך היחידות הניסויות כך שהאומדן אינו זקוק להנחה “לפני ואחרי” כרונולוגית. זה מורכב יותר סטטיסטית, אבל מתאים טוב יותר לפלטפורמה שאינה מבטיחה סדר PUBs. בכל מקרה לא אריץ שוב את אותם counts רק כדי לנסות לקבל מספר “יפה” יותר. רפליקציה צריכה לבדוק השערה מוגדרת ולתקן את הפרוטוקול, לא לשמש מכונת חיפוש מובהקות.

Checklist לקורא שרוצה לבנות ניסוי משלו

  1. הגדירו מראש מה החומרה יכולה ומה אינה יכולה להוכיח.
  2. בדקו prior art ומתמטיקה לפני כתיבת runner.
  3. נעלו גרסאות והפרידו סודות מתיקיית הפרויקט.
  4. הריצו סימולציה ובדיקות יחידה ללא QPU.
  5. בדקו שה־contexts משתמשים במעגלים שווי־עלות.
  6. בחרו layout לפי כיול וקישוריות, לא לפי מספר קיוביטים מרשים.
  7. הקפיאו QPY, bindings, analyzer, thresholds ו־hashes.
  8. בצעו metadata preflight והגדירו תקרת usage קשיחה.
  9. השאירו נקודת hardware אחת, אישור אנושי וללא retry אוטומטי.
  10. פרסמו תוצאה מוגבלת כפי שהיא, כולל באגים ומגבלות metadata.

מה הפרויקט הזה מוכיח על יכולת — בלי לטעון למה שהפיזיקה לא הוכיחה

העבודה הזאת חיברה מחקר ספרות, ביקורת מתמטית, Python, ‏Qiskit, אבטחת סודות, בחירת חומרה לפי telemetry, תכנון ניסויים, ניהול תקציב, מנגנון אישור, hashes, בדיקות, סטטיסטיקה ותיעוד. הערך אינו “נגעתי במחשב עם 156 קיוביטים”. הערך הוא שלקחתי רעיון עמום והפכתי אותו למערכת שניתנת לבדיקה, עם גבולות פעולה וגבולות טענה.

אותה משמעת דרושה כאשר מחברים AI לתהליך ארגוני: הטמעה אמיתית אינה demo נוצץ, אלא שילוב של ארכיטקטורה, IT, אבטחה, מדדים ואישור אנושי. זו בדיוק הגישה שלי ל־הטמעה שמחברת מחקר, IT, אבטחה ומדידה. אפשר גם לקרוא על יישומים אפשריים של מחשוב קוונטי בפיתוח תרופות, אבל חשוב לשמור שם, כמו כאן, על ההבדל בין פוטנציאל לבין תוצאה מוכחת.

חומרה קוונטית אמיתית יכולה לתת נתונים. היא אינה פוטרת אותנו מהחובה להוכיח שהנתונים באמת עונים על השאלה ששאלנו. במקרה הזה הנתונים היו שלמים, המבחנים לא מצאו חריגה מובהקת, והמגבלה בתזמון מנעה הכרזה חזקה יותר. לפרסם זאת ביושר אינו להחליש את העבודה — זו העבודה.

מקורות רשמיים להמשך ולבדיקת התנאים העדכניים

המאמר עצמו נכתב מתוך קוד המחקר, המניפסטים, קבלות ההרצה, קובצי התוצאות וששת מסמכי הביקורת הפנימיים. הקישורים הבאים משמשים לאימות הממשק והתנאים המשתנים של IBM:

בסקירה שערכתי לא מצאתי מדריך עברי עדכני שמחבר באותו עמוד הרשמה, Qiskit, ‏Open Plan, ‏SamplerV2/PUBs, חומרת Kingston, קוד מחקר אמיתי, סטטיסטיקה ותוצאה מוגבלת. לכן בניתי כאן מדריך מעשי מהסוג שכמעט אינו קיים בעברית — בלי להפוך את הטענה הזאת להצהרה מוחלטת שאי אפשר להוכיח.

שאלות נפוצות על IBM Quantum, ‏Qiskit והניסוי

מהו Qiskit, והאם הוא המחשב הקוונטי?

Qiskit הוא SDK בקוד פתוח לבניית מעגלים, סימולציה, transpilation והכנת workloads. הוא אינו ה־QPU. Qiskit IBM Runtime הוא החיבור בין הקוד לבין שירותי החומרה של IBM.

האם באמת מקבלים עשר דקות מחשב קוונטי בחינם?

נכון למועד הפרסום, Open Plan מציע עד עשר דקות QPU בחלון מתגלגל של 28 יום. התנאים יכולים להשתנות, ולכן צריך לבדוק את דף התוכנית לפני הרצה. זמן המתנה בתור אינו זהה לזמן ה־QPU המחויב.

האם השתמשתי בכל 156 הקיוביטים של ibm_kingston?

לא. ה־backend מכיל 156 קיוביטים, והניסוי השתמש בשלושה פיזיים בלבד: 89, 91 ו־90. הם נבחרו לפי קישוריות וכיול. שימוש רק במה שנדרש הוא תכנון נכון, לא חיסרון.

למה נדרשו 458,752 shots?

היו 448 תצורות אפקטיביות, וכל אחת נמדדה 1,024 פעמים. המכפלה היא 458,752. החזרות מאפשרות להעריך הסתברויות ואי־ודאות; הן אינן מבטלות רעש חומרה שיטתי.

האם התוצאה inconclusive אומרת שההרצה נכשלה?

לא. ה־Job הושלם וכל הנתונים התקבלו. המגבלה הייתה בהסקה: חלון execution משותף לא אפשר להוכיח שהכיולים בוצעו פיזית לפני ואחרי הניסוי, ולכן אי אפשר היה להעניק סיווג null validation חזק.

האם מצאנו פיזיקה חדשה או הוכחנו את HQC?

לא. לא נמצאה ראיה מובהקת ל־I3 או I4 שאינם אפס. הניסוי גם לא תוכנן להוכיח את HQC; הוא היה null benchmark של חומרה וצינור ניתוח תחת מכניקת קוונטים רגילה.

אפשר לבצע חלק מהתהליך בלי חשבון IBM?

כן. ההתקנה, Bell test, ‏Statevector, בניית המעגלים, בדיקות היחידה, dry-run והניתוח הסינתטי הם מקומיים. רק הגשת Job ל־QPU דורשת credentials, instance ומכסה זמינה.

מה אסור לפרסם מתוך פרויקט IBM Quantum?

אין לפרסם API key, ‏CRN מלא, מזהי חשבון, קובצי credentials או צילום מסך שחושף אותם. קוד, גרסאות, backend, layout, תוצאות ונתונים סטטיסטיים אפשר לפרסם לאחר סינון פרטיות והקשר מתאים.

צריך לקחת רעיון מורכב ולהפוך אותו למערכת עובדת, מדידה ומאובטחת?
אני מחבר מחקר, קוד, תשתיות, AI, בקרות ותיעוד לתהליך שאפשר לבדוק ולהפעיל באמת. אפשר לפנות אליי לשיחת אפיון ממוקדת.