66
77Provides integration with Amazon Braket devices, enabling automatic tracking
88of quantum circuit execution, results, and device configurations using the
9- Uniform Execution Contract (UEC) 1.0 .
9+ Uniform Execution Contract (UEC).
1010
1111Example
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."""
0 commit comments