Skip to content

Commit 3841c74

Browse files
authored
Merge pull request #18 from devqubit-labs/feat/strict_uec_and_adapters_compliance
feat: UEC as source of truth and adapter compliance
2 parents b274c05 + 6166312 commit 3841c74

30 files changed

Lines changed: 1869 additions & 453 deletions

File tree

changelog.d/16.changed.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
UEC ExecutionEnvelope is now the single source of truth for diffs/verification: adapter runs must emit a schema-valid envelope; “non-strict” fallback/synthesis for adapter runs was removed. Manual/replay runs still synthesize a best-effort envelope. Program comparison now distinguishes structural vs parametric hashes and results are canonicalized across bit orders.

packages/devqubit-braket/src/devqubit_braket/adapter.py

Lines changed: 148 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
77
Provides integration with Amazon Braket devices, enabling automatic tracking
88
of quantum circuit execution, results, and device configurations using the
9-
Uniform Execution Contract (UEC) 1.0.
9+
Uniform Execution Contract (UEC).
1010
1111
Example
1212
-------
@@ -203,7 +203,7 @@ def _materialize_task_spec(
203203
# ============================================================================
204204

205205

206-
def _compute_circuit_hash(circuits: list[Any]) -> str | None:
206+
def _compute_structural_hash(circuits: list[Any]) -> str | None:
207207
"""
208208
Compute a content hash for circuits.
209209
@@ -275,6 +275,109 @@ def _compute_circuit_hash(circuits: list[Any]) -> str | None:
275275
return f"sha256:{hashlib.sha256(payload).hexdigest()}"
276276

277277

278+
def _compute_parametric_hash(
279+
circuits: list[Any],
280+
inputs: dict[str, float] | None = None,
281+
) -> str | None:
282+
"""
283+
Compute a parametric hash for Braket circuits.
284+
285+
Unlike structural hash, this includes actual parameter values,
286+
making it suitable for identifying identical circuit executions.
287+
288+
Parameters
289+
----------
290+
circuits : list[Any]
291+
List of Braket Circuit objects.
292+
inputs : dict[str, float] or None
293+
Parameter bindings for FreeParameters.
294+
295+
Returns
296+
-------
297+
str | None
298+
SHA256 hash with prefix, or None if circuits is empty.
299+
300+
Notes
301+
-----
302+
Includes:
303+
- All structural information (gate types, qubit topology)
304+
- Resolved parameter values from inputs dict
305+
- Unresolved FreeParameter names
306+
"""
307+
if not circuits:
308+
return None
309+
310+
circuit_signatures: list[str] = []
311+
312+
for circuit in circuits:
313+
try:
314+
instrs = getattr(circuit, "instructions", None)
315+
if instrs is None:
316+
circuit_signatures.append(str(circuit)[:500])
317+
continue
318+
319+
op_sigs: list[str] = []
320+
for instr in instrs:
321+
op = getattr(instr, "operator", None)
322+
# Gate name
323+
if op is not None:
324+
op_name = getattr(op, "name", None)
325+
op_name = (
326+
op_name
327+
if isinstance(op_name, str) and op_name
328+
else type(op).__name__
329+
)
330+
else:
331+
op_name = type(instr).__name__
332+
333+
# Get actual parameter values
334+
param_strs: list[str] = []
335+
if op is not None:
336+
for attr in ("parameters", "params", "angles"):
337+
val = getattr(op, attr, None)
338+
if isinstance(val, (list, tuple)):
339+
for p in val:
340+
try:
341+
# Check if it's a FreeParameter
342+
if hasattr(p, "name"):
343+
if inputs and p.name in inputs:
344+
param_strs.append(
345+
f"{float(inputs[p.name]):.10f}"
346+
)
347+
else:
348+
param_strs.append(f"<param:{p.name}>")
349+
else:
350+
param_strs.append(f"{float(p):.10f}")
351+
except (TypeError, ValueError):
352+
param_strs.append(str(p)[:50])
353+
break
354+
355+
# Target qubits
356+
tgt = getattr(instr, "target", None)
357+
if tgt is not None:
358+
try:
359+
targets = tuple(
360+
str(getattr(q, "index", None) or q) for q in tgt
361+
)
362+
except Exception:
363+
targets = (str(tgt),)
364+
else:
365+
targets = ()
366+
367+
params_suffix = (
368+
f"|params=[{','.join(param_strs)}]" if param_strs else ""
369+
)
370+
op_sigs.append(f"{op_name}{params_suffix}|t{targets}")
371+
372+
circuit_signatures.append("||".join(op_sigs))
373+
374+
except Exception:
375+
circuit_signatures.append(str(circuit)[:500])
376+
377+
payload = "\n".join(circuit_signatures).encode("utf-8", errors="replace")
378+
return f"sha256:{hashlib.sha256(payload).hexdigest()}"
379+
380+
278381
# ============================================================================
279382
# TrackedDevice - wraps Braket devices with tracking
280383
# ============================================================================
@@ -359,14 +462,23 @@ def run(
359462
self._execution_count += 1
360463
exec_count = self._execution_count
361464

362-
# Compute circuit hash
363-
circuit_hash = _compute_circuit_hash(circuits_for_logging)
364-
is_new_circuit = circuit_hash and circuit_hash not in self._seen_circuit_hashes
365-
if circuit_hash:
366-
self._seen_circuit_hashes.add(circuit_hash)
465+
# Compute hashes
466+
# structural_hash: ignores parameter values (for deduplication)
467+
# parametric_hash: includes parameter values from inputs (for exact match)
468+
structural_hash = _compute_structural_hash(circuits_for_logging)
469+
470+
# Extract inputs for parametric hash (Braket's FreeParameter bindings)
471+
inputs = kwargs.get("inputs")
472+
parametric_hash = _compute_parametric_hash(circuits_for_logging, inputs)
473+
474+
is_new_circuit = (
475+
structural_hash and structural_hash not in self._seen_circuit_hashes
476+
)
477+
if structural_hash:
478+
self._seen_circuit_hashes.add(structural_hash)
367479

368480
# Determine logging behavior
369-
should_log = self._should_log(exec_count, circuit_hash, is_new_circuit)
481+
should_log = self._should_log(exec_count, structural_hash, is_new_circuit)
370482

371483
# Build execution options
372484
options: dict[str, Any] = {}
@@ -410,17 +522,18 @@ def run(
410522
shots=shots,
411523
task_ids=task_ids,
412524
submitted_at=submitted_at,
413-
circuit_hash=circuit_hash,
525+
structural_hash=structural_hash,
526+
parametric_hash=parametric_hash,
414527
execution_index=exec_count,
415528
options=options if options else None,
416529
)
417530

418-
if circuit_hash:
419-
self._logged_circuit_hashes.add(circuit_hash)
531+
if structural_hash:
532+
self._logged_circuit_hashes.add(structural_hash)
420533

421534
self._logged_execution_count += 1
422535

423-
# Set tracker tags/params (P1 fix: provider is platform, not SDK)
536+
# Set tracker tags/params
424537
self.tracker.set_tag("backend_name", device_name)
425538
self.tracker.set_tag("provider", "aws_braket")
426539
self.tracker.set_tag("adapter", "devqubit-braket")
@@ -442,7 +555,8 @@ def run(
442555
"sdk": "braket",
443556
"num_circuits": len(circuits_for_logging),
444557
"execution_count": exec_count,
445-
"program_hash": circuit_hash,
558+
"structural_hash": structural_hash,
559+
"parametric_hash": parametric_hash,
446560
"shots": shots,
447561
"task_ids": task_ids,
448562
}
@@ -504,14 +618,21 @@ def run_batch(
504618
self._execution_count += 1
505619
exec_count = self._execution_count
506620

507-
# Compute circuit hash
508-
circuit_hash = _compute_circuit_hash(circuits_for_logging)
509-
is_new_circuit = circuit_hash and circuit_hash not in self._seen_circuit_hashes
510-
if circuit_hash:
511-
self._seen_circuit_hashes.add(circuit_hash)
621+
# Compute hashes
622+
structural_hash = _compute_structural_hash(circuits_for_logging)
623+
624+
# Extract inputs for parametric hash (Braket's FreeParameter bindings)
625+
inputs = kwargs.get("inputs")
626+
parametric_hash = _compute_parametric_hash(circuits_for_logging, inputs)
627+
628+
is_new_circuit = (
629+
structural_hash and structural_hash not in self._seen_circuit_hashes
630+
)
631+
if structural_hash:
632+
self._seen_circuit_hashes.add(structural_hash)
512633

513634
# Determine logging behavior
514-
should_log = self._should_log(exec_count, circuit_hash, is_new_circuit)
635+
should_log = self._should_log(exec_count, structural_hash, is_new_circuit)
515636

516637
# Build execution options
517638
options: dict[str, Any] = {
@@ -554,17 +675,18 @@ def run_batch(
554675
shots=shots,
555676
task_ids=[], # Batch doesn't have a single ID upfront
556677
submitted_at=submitted_at,
557-
circuit_hash=circuit_hash,
678+
structural_hash=structural_hash,
679+
parametric_hash=parametric_hash,
558680
execution_index=exec_count,
559681
options=options,
560682
)
561683

562-
if circuit_hash:
563-
self._logged_circuit_hashes.add(circuit_hash)
684+
if structural_hash:
685+
self._logged_circuit_hashes.add(structural_hash)
564686

565687
self._logged_execution_count += 1
566688

567-
# Set tracker tags/params (P1 fix: provider is platform, not SDK)
689+
# Set tracker tags/params
568690
self.tracker.set_tag("backend_name", device_name)
569691
self.tracker.set_tag("provider", "aws_braket")
570692
self.tracker.set_tag("adapter", "devqubit-braket")
@@ -588,7 +710,8 @@ def run_batch(
588710
"sdk": "braket",
589711
"num_circuits": len(circuits_for_logging),
590712
"execution_count": exec_count,
591-
"program_hash": circuit_hash,
713+
"structural_hash": structural_hash,
714+
"parametric_hash": parametric_hash,
592715
"shots": shots,
593716
"batch": True,
594717
}
@@ -614,7 +737,7 @@ def run_batch(
614737
def _should_log(
615738
self,
616739
exec_count: int,
617-
circuit_hash: str | None,
740+
structural_hash: str | None,
618741
is_new_circuit: bool,
619742
) -> bool:
620743
"""Determine if this execution should be logged."""

packages/devqubit-braket/src/devqubit_braket/envelope.py

Lines changed: 29 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
"""
55
Envelope creation for Braket adapter.
66
7-
Creates UEC 1.0 compliant ExecutionEnvelopes with proper snapshots
7+
Creates UEC compliant ExecutionEnvelopes with proper snapshots
88
for device, program, execution, and result data.
99
1010
Notes
@@ -175,7 +175,8 @@ def serialize_and_log_circuits(
175175
def create_program_snapshot(
176176
circuits: list[Any],
177177
artifact_refs: list[ArtifactRef],
178-
circuit_hash: str | None,
178+
structural_hash: str | None,
179+
parametric_hash: str | None = None,
179180
) -> ProgramSnapshot:
180181
"""
181182
Create a ProgramSnapshot from circuits and their artifact refs.
@@ -186,13 +187,15 @@ def create_program_snapshot(
186187
List of Braket circuits.
187188
artifact_refs : list of ArtifactRef
188189
References to logged circuit artifacts.
189-
circuit_hash : str or None
190-
Circuit structure hash.
190+
structural_hash : str or None
191+
Structural hash (ignores parameter values).
192+
parametric_hash : str or None
193+
Parametric hash (includes parameter values).
191194
192195
Returns
193196
-------
194197
ProgramSnapshot
195-
Program snapshot with logical artifacts.
198+
Program snapshot with logical artifacts and hashes.
196199
"""
197200
logical_artifacts: list[ProgramArtifact] = []
198201

@@ -211,10 +214,17 @@ def create_program_snapshot(
211214
)
212215
)
213216

217+
# If parametric_hash not provided, use structural_hash
218+
effective_parametric_hash = parametric_hash or structural_hash
219+
214220
return ProgramSnapshot(
215221
logical=logical_artifacts,
216222
physical=[], # Braket doesn't expose transpiled circuits
217-
program_hash=circuit_hash,
223+
structural_hash=structural_hash,
224+
parametric_hash=effective_parametric_hash,
225+
# For Braket without transpilation, executed hashes equal logical
226+
executed_structural_hash=structural_hash,
227+
executed_parametric_hash=effective_parametric_hash,
218228
num_circuits=len(circuits),
219229
)
220230

@@ -268,7 +278,7 @@ def create_result_snapshot(
268278
error: Exception | None = None,
269279
) -> ResultSnapshot:
270280
"""
271-
Create a ResultSnapshot from Braket result (UEC 1.0 format).
281+
Create a ResultSnapshot from Braket result.
272282
273283
Parameters
274284
----------
@@ -313,7 +323,7 @@ def create_result_snapshot(
313323
counts_data = exp.get("counts", {})
314324
item_success = bool(counts_data)
315325

316-
# Build counts structure per UEC 1.0 schema
326+
# Build counts structure
317327
counts_obj = None
318328
if counts_data:
319329
counts_obj = {
@@ -370,7 +380,8 @@ def create_envelope(
370380
shots: int | None,
371381
task_ids: list[str],
372382
submitted_at: str,
373-
circuit_hash: str | None,
383+
structural_hash: str | None,
384+
parametric_hash: str | None = None,
374385
execution_index: int = 1,
375386
options: dict[str, Any] | None = None,
376387
) -> ExecutionEnvelope:
@@ -391,8 +402,10 @@ def create_envelope(
391402
Task identifiers.
392403
submitted_at : str
393404
Submission timestamp.
394-
circuit_hash : str or None
395-
Circuit hash.
405+
structural_hash : str or None
406+
Structural hash (ignores parameter values).
407+
parametric_hash : str or None
408+
Parametric hash (includes parameter values).
396409
execution_index : int
397410
Which execution this is (1-indexed sequence number).
398411
options : dict, optional
@@ -444,7 +457,8 @@ def create_envelope(
444457
program_snapshot = create_program_snapshot(
445458
circuits=circuits,
446459
artifact_refs=artifact_refs,
447-
circuit_hash=circuit_hash,
460+
structural_hash=structural_hash,
461+
parametric_hash=parametric_hash,
448462
)
449463

450464
# Create execution snapshot
@@ -456,7 +470,7 @@ def create_envelope(
456470
options=options,
457471
)
458472

459-
# Create ProducerInfo for UEC 1.0
473+
# Create ProducerInfo
460474
sdk_version = braket_version()
461475
producer = ProducerInfo.create(
462476
adapter="devqubit-braket",
@@ -466,7 +480,7 @@ def create_envelope(
466480
frontends=["braket-sdk"],
467481
)
468482

469-
# Create pending result (UEC 1.0 requires result field)
483+
# Create pending result
470484
pending_result = ResultSnapshot(
471485
success=False,
472486
status="failed", # Will be updated by finalize_envelope
@@ -597,7 +611,7 @@ def finalize_envelope(
597611
except Exception as e:
598612
logger.debug("Failed to log counts: %s", e)
599613

600-
# Update tracker record (UEC 1.0 fields)
614+
# Update tracker record
601615
tracker.record["results"] = {
602616
"completed_at": utc_now_iso(),
603617
"backend_name": device_name,

0 commit comments

Comments
 (0)