qia-check 1.0.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1 @@
1
+ evidence/otsenv/
@@ -0,0 +1,10 @@
1
+ cff-version: 1.2.0
2
+ message: "If you use this package, please cite it."
3
+ title: "QIA Research Package: checks on IBM Quantum calibration records before and after a run"
4
+ authors:
5
+ - name: "Heuremen LLC"
6
+ version: 1.0.0
7
+ date-released: 2026-11-01
8
+ url: "https://indahl.ai/qia/package"
9
+ repository-code: "https://github.com/Heuremen-LLC/indahl.ai"
10
+ keywords: [quantum computing, calibration, IBM Quantum, T1, T2, reproducibility]
@@ -0,0 +1,15 @@
1
+ Heuremen LLC
2
+ https://indahl.ai/qia
3
+
4
+ Code
5
+ Heuremen LLC permits anyone to run, copy and change the code in this package, for any purpose, with attribution to Heuremen LLC and https://indahl.ai/qia
6
+ The code is provided as it is, without warranty of any kind.
7
+
8
+ Text and data
9
+ The text and data in this package (the README, TESTED.md and any example or test data) are free to download, read and cite with attribution (Heuremen LLC, with a link to https://indahl.ai/qia).
10
+ All rights are reserved otherwise. Text and data mining, and use of this text or data to train, fine-tune or build AI models, are not permitted.
11
+ This is an express reservation of rights, also made in machine-readable form at https://indahl.ai/robots.txt and https://indahl.ai/.well-known/tdmrep.json.
12
+ Policy: https://indahl.ai/terms#ai-training
13
+
14
+ Your results
15
+ Your files and results remain yours.
@@ -0,0 +1,94 @@
1
+ Metadata-Version: 2.5
2
+ Name: qia-check
3
+ Version: 1.0.0
4
+ Summary: Checks on IBM Quantum calibration records before and after a run, from Heuremen LLC. Standard library only.
5
+ Project-URL: Homepage, https://indahl.ai/qia
6
+ Project-URL: Documentation, https://indahl.ai/qia/package
7
+ Project-URL: Source, https://github.com/Heuremen-LLC/indahl.ai
8
+ Author: Heuremen LLC
9
+ License: Heuremen LLC
10
+ https://indahl.ai/qia
11
+
12
+ Code
13
+ Heuremen LLC permits anyone to run, copy and change the code in this package, for any purpose, with attribution to Heuremen LLC and https://indahl.ai/qia
14
+ The code is provided as it is, without warranty of any kind.
15
+
16
+ Text and data
17
+ The text and data in this package (the README, TESTED.md and any example or test data) are free to download, read and cite with attribution (Heuremen LLC, with a link to https://indahl.ai/qia).
18
+ All rights are reserved otherwise. Text and data mining, and use of this text or data to train, fine-tune or build AI models, are not permitted.
19
+ This is an express reservation of rights, also made in machine-readable form at https://indahl.ai/robots.txt and https://indahl.ai/.well-known/tdmrep.json.
20
+ Policy: https://indahl.ai/terms#ai-training
21
+
22
+ Your results
23
+ Your files and results remain yours.
24
+ License-File: LICENSE
25
+ Keywords: IBM Quantum,T1,T2,calibration,quantum computing,reproducibility
26
+ Classifier: Environment :: Console
27
+ Classifier: Intended Audience :: Science/Research
28
+ Classifier: License :: Other/Proprietary License
29
+ Classifier: Operating System :: OS Independent
30
+ Classifier: Programming Language :: Python :: 3
31
+ Requires-Python: >=3.9
32
+ Provides-Extra: ibm
33
+ Requires-Dist: qiskit-ibm-runtime; extra == 'ibm'
34
+ Description-Content-Type: text/markdown
35
+
36
+ # qia-check
37
+
38
+ Heuremen LLC · https://indahl.ai/qia
39
+
40
+ Checks on IBM Quantum calibration figures before a run and after it. Your files are opened read-only and never changed. Reports go to a separate folder. Run on saved files, the checker sends nothing anywhere. Only the `--pull` option contacts IBM, with your own saved account, and only when you run it. Python 3.9 or later, standard library only.
41
+
42
+ ## Install
43
+
44
+ ```
45
+ pip install qia-check
46
+ ```
47
+
48
+ For the `--pull` option, which saves the live record from IBM, add the optional IBM client:
49
+
50
+ ```
51
+ pip install "qia-check[ibm]"
52
+ ```
53
+
54
+ The tool is then the command `qia-check`. It is also importable as the module `qia_check`.
55
+
56
+ ## Before a run
57
+
58
+ 1. Save the record you are about to run under. This needs the IBM client (`pip install "qia-check[ibm]"`) and a saved IBM Quantum account. Let the checker do it:
59
+ `qia-check before --pull ibm_kingston`
60
+ Or save it yourself with `QiskitRuntimeService().backend('ibm_kingston').properties().to_dict()` written out as JSON.
61
+ 2. Check it: `qia-check before record.json --qubits 0,1,5`
62
+ Flags: T1 or T2 measured more than 24 hours before the record's stamp, error values of exactly 0, T2 above 2 x T1, and T1 and T2 measured more than 24 hours apart.
63
+ 3. Read the Guards before the work: `pip install faultsguards`, then `python -m faultsguards guards`
64
+
65
+ ## After a run
66
+
67
+ 1. Save the record again after the run, into the same folder as the first. Keep reports in a different folder.
68
+ 2. `qia-check after records/ --start 2026-11-01T12:00:00Z --end 2026-11-01T12:40:00Z --qubits 0,1,5`
69
+ Times are UTC, written with Z or an offset. The report names the latest record in the folder stamped at or before the start, lists every newer record stamped up to the end, and shows each change in the T1, T2 and readout-error figures on your qubits from one record to the next. If IBM recalibrated between your last saved record and the run, the record in force is not in the folder, so save one right before you submit.
70
+ 3. Read your write-up against the Faults: `python -m faultsguards faults`
71
+
72
+ ## The rest of the QIA package
73
+
74
+ The full package adds the YO record reader, the same-job T1 and T2 measurement, the check block and the paper. It is at https://indahl.ai/qia/package
75
+
76
+ ## How this was tested
77
+
78
+ See TESTED.md in the source distribution, installed under `share/doc/qia-check/`, and published at https://indahl.ai/qia/package
79
+
80
+ ## Links
81
+
82
+ - QIA: https://indahl.ai/qia
83
+ - Package page: https://indahl.ai/qia/package
84
+ - List of studies: https://indahl.ai/qia/list
85
+ - Key: https://indahl.ai/qia/key
86
+ - Code: https://github.com/Heuremen-LLC/indahl.ai
87
+ - Faults and Guards on PyPI: https://pypi.org/project/faultsguards/
88
+ - YO: https://huggingface.co/indahl/YO
89
+
90
+ ## Terms
91
+
92
+ Heuremen LLC permits anyone to run, copy and change the code in this package, for any purpose, with attribution to Heuremen LLC and https://indahl.ai/qia
93
+
94
+ Your results remain yours. The text and data follow LICENSE, and their use to train, fine-tune or build AI models is not permitted.
@@ -0,0 +1,59 @@
1
+ # qia-check
2
+
3
+ Heuremen LLC · https://indahl.ai/qia
4
+
5
+ Checks on IBM Quantum calibration figures before a run and after it. Your files are opened read-only and never changed. Reports go to a separate folder. Run on saved files, the checker sends nothing anywhere. Only the `--pull` option contacts IBM, with your own saved account, and only when you run it. Python 3.9 or later, standard library only.
6
+
7
+ ## Install
8
+
9
+ ```
10
+ pip install qia-check
11
+ ```
12
+
13
+ For the `--pull` option, which saves the live record from IBM, add the optional IBM client:
14
+
15
+ ```
16
+ pip install "qia-check[ibm]"
17
+ ```
18
+
19
+ The tool is then the command `qia-check`. It is also importable as the module `qia_check`.
20
+
21
+ ## Before a run
22
+
23
+ 1. Save the record you are about to run under. This needs the IBM client (`pip install "qia-check[ibm]"`) and a saved IBM Quantum account. Let the checker do it:
24
+ `qia-check before --pull ibm_kingston`
25
+ Or save it yourself with `QiskitRuntimeService().backend('ibm_kingston').properties().to_dict()` written out as JSON.
26
+ 2. Check it: `qia-check before record.json --qubits 0,1,5`
27
+ Flags: T1 or T2 measured more than 24 hours before the record's stamp, error values of exactly 0, T2 above 2 x T1, and T1 and T2 measured more than 24 hours apart.
28
+ 3. Read the Guards before the work: `pip install faultsguards`, then `python -m faultsguards guards`
29
+
30
+ ## After a run
31
+
32
+ 1. Save the record again after the run, into the same folder as the first. Keep reports in a different folder.
33
+ 2. `qia-check after records/ --start 2026-11-01T12:00:00Z --end 2026-11-01T12:40:00Z --qubits 0,1,5`
34
+ Times are UTC, written with Z or an offset. The report names the latest record in the folder stamped at or before the start, lists every newer record stamped up to the end, and shows each change in the T1, T2 and readout-error figures on your qubits from one record to the next. If IBM recalibrated between your last saved record and the run, the record in force is not in the folder, so save one right before you submit.
35
+ 3. Read your write-up against the Faults: `python -m faultsguards faults`
36
+
37
+ ## The rest of the QIA package
38
+
39
+ The full package adds the YO record reader, the same-job T1 and T2 measurement, the check block and the paper. It is at https://indahl.ai/qia/package
40
+
41
+ ## How this was tested
42
+
43
+ See TESTED.md in the source distribution, installed under `share/doc/qia-check/`, and published at https://indahl.ai/qia/package
44
+
45
+ ## Links
46
+
47
+ - QIA: https://indahl.ai/qia
48
+ - Package page: https://indahl.ai/qia/package
49
+ - List of studies: https://indahl.ai/qia/list
50
+ - Key: https://indahl.ai/qia/key
51
+ - Code: https://github.com/Heuremen-LLC/indahl.ai
52
+ - Faults and Guards on PyPI: https://pypi.org/project/faultsguards/
53
+ - YO: https://huggingface.co/indahl/YO
54
+
55
+ ## Terms
56
+
57
+ Heuremen LLC permits anyone to run, copy and change the code in this package, for any purpose, with attribution to Heuremen LLC and https://indahl.ai/qia
58
+
59
+ Your results remain yours. The text and data follow LICENSE, and their use to train, fine-tune or build AI models is not permitted.
@@ -0,0 +1,20 @@
1
+ # How this package was tested
2
+
3
+ Heuremen LLC, 2 November 2026, on the package as published at https://indahl.ai/qia/qia-research-package.zip
4
+
5
+ 1. Against our mirror's audit code. For the latest published calibration record of each of seven IBM processors (ibm_aachen, ibm_boston,
6
+ ibm_fez, ibm_kingston, ibm_marrakesh, ibm_phoenix, ibm_pittsburgh), `qia_check.py before` was compared with the separate audit code that
7
+ runs our public IBM mirror (https://github.com/Heuremen-LLC/indahl.ai/tree/main/code/mirror). The T2 above 2 x T1 flags, with their
8
+ outcomes, and the T1 and T2 date-gap flags agreed qubit for qubit on all seven. Both were written by us to the same rules, so this shows
9
+ the two implementations agree. It does not test the rules themselves.
10
+ 2. Against 362 saved ibm_kingston records. The before checks were recounted with separate code, and every count agreed on every record.
11
+ 3. On a real run. Our Darkwave kit test jobs ran on ibm_kingston from 01:11 to 01:17 UTC on 2 November 2026, and each job saved the
12
+ calibration record it ran under. `qia_check.py after`, given the six records our mirror saved that evening, named the same record
13
+ (stamped 00:39 UTC) and reported no record stamped during the run. No record was stamped in that window, so the part of the check that
14
+ lists records stamped during a run was exercised on test files only, not on this run.
15
+ 4. The pull path. `qia_check.py before --pull ibm_kingston` saved the live record from IBM and checked it. The saved file was unchanged
16
+ after the check (sha256 before and after equal). Our test passed our own IBM connection in place of a saved account. The saved-account
17
+ step itself (QiskitRuntimeService with no arguments) is IBM's own code.
18
+
19
+ Not yet tested inside this package: the YO reader (`yo/yo_audit.py`) against the model. YO itself is scored against the exact code
20
+ answer on held-out IBM records at https://indahl.ai/iris/lab
@@ -0,0 +1,42 @@
1
+ [build-system]
2
+ requires = ["hatchling>=1.18"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "qia-check"
7
+ version = "1.0.0"
8
+ description = "Checks on IBM Quantum calibration records before and after a run, from Heuremen LLC. Standard library only."
9
+ readme = "README.md"
10
+ requires-python = ">=3.9"
11
+ license = { file = "LICENSE" }
12
+ authors = [{ name = "Heuremen LLC" }]
13
+ keywords = ["quantum computing", "calibration", "IBM Quantum", "T1", "T2", "reproducibility"]
14
+ classifiers = [
15
+ "Programming Language :: Python :: 3",
16
+ "Operating System :: OS Independent",
17
+ "Environment :: Console",
18
+ "Intended Audience :: Science/Research",
19
+ "License :: Other/Proprietary License",
20
+ ]
21
+ dependencies = []
22
+
23
+ [project.optional-dependencies]
24
+ ibm = ["qiskit-ibm-runtime"]
25
+
26
+ [project.urls]
27
+ Homepage = "https://indahl.ai/qia"
28
+ Documentation = "https://indahl.ai/qia/package"
29
+ Source = "https://github.com/Heuremen-LLC/indahl.ai"
30
+
31
+ [project.scripts]
32
+ qia-check = "qia_check:main"
33
+
34
+ [tool.hatch.build.targets.wheel]
35
+ packages = ["src/qia_check.py"]
36
+
37
+ [tool.hatch.build.targets.wheel.shared-data]
38
+ "TESTED.md" = "share/doc/qia-check/TESTED.md"
39
+ "CITATION.cff" = "share/doc/qia-check/CITATION.cff"
40
+
41
+ [tool.hatch.build.targets.sdist]
42
+ include = ["src/qia_check.py", "README.md", "LICENSE", "TESTED.md", "CITATION.cff", "pyproject.toml"]
@@ -0,0 +1,153 @@
1
+ #!/usr/bin/env python3
2
+ """qia_check.py -- QIA Research Package, Heuremen LLC (https://indahl.ai/qia). Standard library only.
3
+
4
+ Checks the IBM Quantum calibration record your run depends on, before the run and after it. Your files are opened read-only and are
5
+ never changed. Their sha256 is taken before and after and printed in the report. Reports go to a separate folder (default
6
+ ./heuremen_report/). Run on saved files, it sends nothing anywhere. Only --pull contacts IBM, with your own saved account.
7
+ Python 3.9 or later. Times are UTC: give them with Z or an offset (2026-11-01T12:00:00Z).
8
+
9
+ BEFORE A RUN python qia_check.py before RECORD.json [--qubits 0,1,5]
10
+ AFTER A RUN python qia_check.py after RECORD_DIR --start 2026-11-01T12:00:00Z --end 2026-11-01T12:40:00Z [--qubits 0,1,5]
11
+
12
+ RECORD.json is a backend's calibration record in Qiskit's BackendProperties format, saved with
13
+ json.dump(backend.properties().to_dict(), open('record.json', 'w'), default=str)
14
+ (.json.gz also works). RECORD_DIR holds several such records from the same backend, for example one saved before and one after your run.
15
+ Optional: --pull BACKEND saves the current record with your own saved IBM account (pip install qiskit-ibm-runtime) and then checks it.
16
+
17
+ Before: for each qubit (all, or --qubits), the record's own figures are checked:
18
+ STALE T1 or T2 measured more than 24 hours before the record's last_update_date
19
+ ZERO an error value (readout_error, prob_meas1_prep0, prob_meas0_prep1) of exactly 0
20
+ T2>2T1 a published pair with T2 above 2 x T1 (a single-exponential T2 cannot exceed 2 x T1). Above 2.5 x T1 reads ABOVE; between 2 and
21
+ 2.5 reads CANNOT_TELL. IBM publishes no fit width with the figures, so a band is needed, and we use 2.5 as its edge
22
+ PAIR T1 and T2 measured more than 24 hours apart
23
+ After: the latest record in the folder stamped at or before --start, every newer record stamped up to --end, and each change in the
24
+ T1, T2 and readout-error figures on your qubits from one record to the next. If IBM recalibrated between your last saved record and the
25
+ run, the record in force is not in the folder, so save one right before you submit. Compare your results against the record in force.
26
+
27
+ The checks read IBM's published figures only. They make no claim about your results. Free to run and cite with attribution; see LICENSE.txt."""
28
+ import json, gzip, sys, os, glob, hashlib, argparse, html, datetime as dt
29
+ UTC = dt.timezone.utc
30
+ ERRS = ('readout_error', 'prob_meas1_prep0', 'prob_meas0_prep1'); FIELDS = ('T1', 'T2') + ERRS
31
+ NAME = 'QIA Research Package'; ORG = 'Heuremen LLC'; SITE = 'https://indahl.ai/qia'
32
+ LOGO = ('<svg viewBox="0 0 200 200" width="44" height="44" aria-hidden="true" fill="none" stroke="currentColor" stroke-width="9">'
33
+ '<polygon points="100,30 180,170 20,170"/><polygon points="100,170 139,84 61,84"/><circle cx="100" cy="112" r="5" fill="currentColor"/></svg>')
34
+ def when(s):
35
+ d = dt.datetime.fromisoformat(str(s).replace('Z', '+00:00')); return (d if d.tzinfo else d.replace(tzinfo=UTC)).astimezone(UTC)
36
+ def sha(p):
37
+ h = hashlib.sha256()
38
+ with open(p, 'rb') as f:
39
+ for b in iter(lambda: f.read(1 << 20), b''): h.update(b)
40
+ return h.hexdigest()
41
+ def load(p):
42
+ with (gzip.open(p, 'rt', encoding='utf-8') if p.endswith('.gz') else open(p, 'r', encoding='utf-8')) as f: d = json.load(f)
43
+ if not isinstance(d, dict) or 'last_update_date' not in d or not isinstance(d.get('qubits'), list): raise ValueError('not a calibration record')
44
+ Q = {}
45
+ for i, qs in enumerate(d.get('qubits', [])):
46
+ Q[i] = {x['name']: (x.get('value'), x.get('date'), x.get('unit', '')) for x in qs if x.get('name') in FIELDS}
47
+ return d.get('backend_name', '?'), when(d['last_update_date']), Q
48
+ def check_record(S, Q, qubits):
49
+ rows = []
50
+ for q in qubits:
51
+ e = Q.get(q, {}); t1, t2 = e.get('T1'), e.get('T2')
52
+ for f in ('T1', 'T2'):
53
+ if e.get(f) and e[f][1] and (S - when(e[f][1])).total_seconds() > 86400:
54
+ rows.append(dict(qubit=q, check='STALE', field=f, detail=f'measured {when(e[f][1]):%Y-%m-%d %H:%M} UTC, {(S - when(e[f][1])).total_seconds() / 3600:.1f} h before the stamp'))
55
+ for f in ERRS:
56
+ if e.get(f) and e[f][0] == 0: rows.append(dict(qubit=q, check='ZERO', field=f, detail='published value exactly 0'))
57
+ if t1 and t2 and t1[0] and t2[0] is not None and t1[0] > 0:
58
+ r = t2[0] / t1[0]
59
+ if r > 2.0: rows.append(dict(qubit=q, check='T2>2T1', field='T2/T1', detail=f'ratio {r:.3f} (T1 {t1[0]:.4g}, T2 {t2[0]:.4g})', outcome='ABOVE' if r > 2.5 else 'CANNOT_TELL'))
60
+ if t1[1] and t2[1]:
61
+ gap = abs((when(t1[1]) - when(t2[1])).total_seconds()) / 3600
62
+ if gap > 24: rows.append(dict(qubit=q, check='PAIR', field='T1/T2 dates', detail=f'{gap:.1f} h apart'))
63
+ return rows
64
+ def page(title, meta, sections, inputs):
65
+ css = ('body{font:16px/1.5 Georgia,serif;background:#f6efe2;color:#1c1712;margin:0}main{max-width:900px;margin:0 auto;padding:24px 16px}'
66
+ 'header{display:flex;gap:12px;align-items:center;border-bottom:2px solid #c9a227;padding-bottom:12px}header b{font-size:22px}'
67
+ 'table{border-collapse:collapse;width:100%;font-size:14px}td,th{border-bottom:1px solid #d8ccb4;padding:4px 6px;text-align:left}'
68
+ '.note{color:#5a4d3a;font-size:14px}footer{margin-top:28px;border-top:1px solid #d8ccb4;padding-top:10px;font-size:13px;color:#5a4d3a}')
69
+ h = [f'<!doctype html><html lang="en"><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>{html.escape(title)}</title><style>{css}</style><main>',
70
+ f'<header>{LOGO}<div><b>{NAME}</b><br><span class="note">{ORG} &middot; <a href="{SITE}">{SITE}</a></span></div></header>', f'<h1>{html.escape(title)}</h1>']
71
+ h += [f'<p class="note">{html.escape(m)}</p>' for m in meta]
72
+ for st, rows, cols in sections:
73
+ h.append(f'<h2>{html.escape(st)}</h2>')
74
+ if not rows: h.append('<p>Nothing flagged.</p>'); continue
75
+ h.append('<table><tr>' + ''.join(f'<th>{html.escape(c)}</th>' for c in cols) + '</tr>' + ''.join('<tr>' + ''.join(f'<td>{html.escape(str(r.get(c, "")))}</td>' for c in cols) + '</tr>' for r in rows) + '</table>')
76
+ h.append('<footer>Your files were opened read-only and not changed. sha256 before and after:<br>' + '<br>'.join(f'{html.escape(os.path.basename(p))}: {a[:16]}&hellip; / {b[:16]}&hellip; {"unchanged" if a == b else "CHANGED"}' for p, a, b in inputs) +
77
+ f'<br>These checks read IBM&rsquo;s published figures only and make no claim about your results. {ORG}, {SITE}</footer></main></html>')
78
+ return '\n'.join(h)
79
+ def write(out, stem, title, meta, sections, inputs, data):
80
+ os.makedirs(out, exist_ok=True); t = dt.datetime.now(UTC).strftime('%Y%m%dT%H%M%S%fZ')
81
+ hp, jp = os.path.join(out, f'{stem}_{t}.html'), os.path.join(out, f'{stem}_{t}.json')
82
+ with open(hp, 'w', encoding='utf-8') as f: f.write(page(title, meta, sections, inputs))
83
+ with open(jp, 'w', encoding='utf-8') as f:
84
+ json.dump(dict(package=NAME, by=ORG, site=SITE, title=title, meta=meta, inputs=[dict(file=os.path.basename(p), sha256_before=a, sha256_after=b) for p, a, b in inputs], **data), f, indent=1, default=str)
85
+ return hp, jp
86
+ def qlist(s, Q):
87
+ if not s: return sorted(Q), []
88
+ try: qs = [int(x) for x in s.split(',') if x.strip()]
89
+ except ValueError: sys.exit('--qubits takes qubit numbers separated by commas, for example 0,1,5')
90
+ return [q for q in qs if q in Q], [q for q in qs if q not in Q]
91
+ def utc(s, name):
92
+ try: d = dt.datetime.fromisoformat(s.replace('Z', '+00:00'))
93
+ except ValueError: sys.exit(f'{name}: give a UTC time like 2026-11-01T12:00:00Z')
94
+ if d.tzinfo is None: sys.exit(f'{name}: add Z or an offset, so the time is not read in the wrong zone')
95
+ return d.astimezone(UTC)
96
+ def fmt(v): return '' if v is None else (f'{v:.4g}' if isinstance(v, (int, float)) else str(v))
97
+ def pull(backend, dest):
98
+ from qiskit_ibm_runtime import QiskitRuntimeService # your own saved account
99
+ b = QiskitRuntimeService().backend(backend); p = os.path.join(dest, f'{backend}_{dt.datetime.now(UTC):%Y%m%dT%H%M%SZ}.json')
100
+ os.makedirs(dest, exist_ok=True); json.dump(b.properties().to_dict(), open(p, 'w'), default=str); return p
101
+ def main():
102
+ ap = argparse.ArgumentParser(description=NAME + ', ' + ORG); sub = ap.add_subparsers(dest='cmd', required=True)
103
+ a1 = sub.add_parser('before'); a1.add_argument('record', nargs='?'); a1.add_argument('--qubits'); a1.add_argument('--pull'); a1.add_argument('--out', default='heuremen_report')
104
+ a2 = sub.add_parser('after'); a2.add_argument('records'); a2.add_argument('--start', required=True); a2.add_argument('--end', required=True); a2.add_argument('--qubits'); a2.add_argument('--out', default='heuremen_report')
105
+ a = ap.parse_args()
106
+ if a.cmd == 'before':
107
+ rec = a.record or (pull(a.pull, os.path.join(a.out, 'records')) if a.pull else sys.exit('give RECORD.json or --pull BACKEND'))
108
+ h0 = sha(rec)
109
+ try: b, S, Q = load(rec)
110
+ except (ValueError, KeyError, json.JSONDecodeError): sys.exit(rec + ' is not a calibration record (Qiskit BackendProperties JSON)')
111
+ qs, miss = qlist(a.qubits, Q); rows = check_record(S, Q, qs); h1 = sha(rec)
112
+ age = (dt.datetime.now(UTC) - S).total_seconds() / 3600
113
+ meta = [f'Backend {b}, record stamped {S:%Y-%m-%d %H:%M} UTC, {age:.1f} h before this check. Qubits checked: {len(qs)}.',
114
+ 'IBM recalibrates during the day. Save the record again right before you submit, and keep it with your data.'] + ([f'Not in this record, so not checked: qubits {", ".join(map(str, miss))}.'] if miss else [])
115
+ cols = ['qubit', 'check', 'field', 'detail', 'outcome']
116
+ hp, jp = write(a.out, f'before_{b}', f'Before the run: {b}', meta, [(f'Flags ({len(rows)})', rows, cols)], [(rec, h0, h1)], dict(backend=b, stamp=S.isoformat(), qubits=qs, flags=rows))
117
+ else:
118
+ rd = os.path.realpath(a.records); od = os.path.realpath(a.out)
119
+ if od == rd or od.startswith(rd + os.sep): sys.exit('--out must be outside the records folder')
120
+ st = utc(a.start, '--start'); en = utc(a.end, '--end')
121
+ if en < st: sys.exit('--end is earlier than --start')
122
+ fs = sorted(glob.glob(os.path.join(a.records, '*.json')) + glob.glob(os.path.join(a.records, '*.json.gz')))
123
+ if not fs: sys.exit('no records in ' + a.records)
124
+ hs = {p: sha(p) for p in fs}; recs, skipped, seen = [], [], set()
125
+ for p in fs:
126
+ try: r = load(p)
127
+ except (ValueError, KeyError, json.JSONDecodeError, OSError): skipped.append(os.path.basename(p)); continue
128
+ if (r[0], r[1]) in seen: skipped.append(os.path.basename(p) + ' (same stamp as another file)'); continue
129
+ seen.add((r[0], r[1])); recs.append(r + (p,))
130
+ recs.sort(key=lambda r: r[1])
131
+ under = [r for r in recs if r[1] <= st]; during = [r for r in recs if st < r[1] <= en]
132
+ meta = [f'Run window {st:%Y-%m-%d %H:%M} to {en:%Y-%m-%d %H:%M} UTC. Records read: {len(recs)}.'] + ([f'Not read as records: {", ".join(skipped)}.'] if skipped else [])
133
+ secs = []; data = {}
134
+ if not under: meta.append('No record in the folder is stamped at or before the run start, so the record in force at the start is not here. Save the record right before you submit.')
135
+ else:
136
+ b, S, Q, p = under[-1]; qs, miss = qlist(a.qubits, Q)
137
+ meta.append(f'The latest record in this folder stamped at or before the start: {S:%Y-%m-%d %H:%M} UTC ({os.path.basename(p)}). If IBM recalibrated after it and before the run, the record in force is not in the folder.')
138
+ if miss: meta.append(f'Not in this record, so not checked: qubits {", ".join(map(str, miss))}.')
139
+ secs.append(('Flags in that record', check_record(S, Q, qs), ['qubit', 'check', 'field', 'detail', 'outcome']))
140
+ ch = []; prev = (S, Q)
141
+ for b2, S2, Q2, p2 in during:
142
+ for q in qs:
143
+ for f in FIELDS:
144
+ v1, v2 = prev[1].get(q, {}).get(f), Q2.get(q, {}).get(f)
145
+ if (v1 or v2) and (v1 or (None, None))[:2] != (v2 or (None, None))[:2]:
146
+ ch.append(dict(record=f'{S2:%Y-%m-%d %H:%M}', qubit=q, field=f, before=fmt(v1 and v1[0]), after=fmt(v2 and v2[0]), date_before=(v1 or ('', ''))[1], date_after=(v2 or ('', ''))[1]))
147
+ prev = (S2, Q2)
148
+ meta.append(f'Records stamped during the run: {len(during)}.' + (' T1, T2 or readout-error figures on your qubits changed during the run; see the table.' if ch else ''))
149
+ secs.append(('T1, T2 and readout-error figures that changed during the run (each record against the one before it)', ch, ['record', 'qubit', 'field', 'before', 'after', 'date_before', 'date_after']))
150
+ data = dict(backend=b, record_at_start=S.isoformat(), records_during=[r[1].isoformat() for r in during], changes=ch, skipped=skipped)
151
+ hp, jp = write(a.out, 'after', 'After the run: the record at the start, and what changed', meta, secs, [(p, hs[p], sha(p)) for p in fs], data)
152
+ print(NAME, '|', ORG, '|', SITE); print('report:', hp); print('data: ', jp)
153
+ if __name__ == '__main__': main()