Script Debugging
Guide to debugging quantum scripts and workflows on the Marqov platform.
Temporal Sandbox Restrictions
Temporal enforces a deterministic sandbox on workflow code. This has direct implications for how Marqov scripts are structured.
What you cannot do in a @workflow function
- Import
marqovor any marqov submodule - Import third-party libraries (numpy, qiskit, etc.)
- Make network calls or access the filesystem
- Use
datetime.now()or other non-deterministic operations
What you can do in a @workflow function
- Call
@task-decorated functions (these become Temporal activities) - Use built-in Python types (str, int, dict, list)
- Perform pure computation on primitive types
Correct pattern
from marqov import task, workflow
@task
def run_circuit(theta):
# All marqov imports and heavy computation happen here
from marqov import Circuit, LocalExecutor
circuit = Circuit().ry(theta, 0).cnot(0, 1)
executor = LocalExecutor()
import asyncio
result = asyncio.run(executor.execute(circuit, shots=1000))
return result.counts
@workflow
def vqe_step(theta):
# This function only composes tasks -- no marqov imports
counts = run_circuit(theta)
return countsIncorrect pattern (will fail)
from marqov import task, workflow, Circuit # Circuit import here is fine at module level
@workflow
def vqe_step(theta):
circuit = Circuit().ry(theta, 0) # FAILS: Circuit() in workflow sandbox
counts = run_circuit(circuit)
return countsSerialization Requirements
All data passed between @task and @workflow functions must be JSON-serializable, because Temporal serializes activity inputs/outputs as JSON.
Supported types
str,int,float,bool,Nonelist,dict(with serializable values)
Types that will fail
marqov.Circuit— usecircuit.to_dict()/Circuit.from_dict(data)numpy.ndarray— convert to list with.tolist()ExecutionResult— extract.counts(a dict)- Custom classes — convert to dict
Example: passing a circuit through Temporal
@task
def build_circuit(theta):
from marqov import Circuit
circuit = Circuit().ry(theta, 0).cnot(0, 1)
return circuit.to_dict() # Returns JSON-serializable dict
@task
def run_circuit(circuit_dict, shots):
from marqov import Circuit, LocalExecutor
circuit = Circuit.from_dict(circuit_dict) # Reconstruct
import asyncio
result = asyncio.run(LocalExecutor().execute(circuit, shots=shots))
return result.counts
@workflow
def my_workflow(theta):
circuit_data = build_circuit(theta)
counts = run_circuit(circuit_data, 1000)
return countsUsing the Temporal UI for Debugging
The Temporal UI provides workflow history, input/output inspection, and retry controls.
Accessing the UI
- Local:
http://localhost:8088 - Production: Configured via
NEXT_PUBLIC_TEMPORAL_UI_URL
Key views
-
Workflow list: See all running and completed workflows. Filter by status, workflow type, or time range.
-
Workflow detail: Click a workflow to see:
- Input parameters (the serialized transport graph)
- Execution timeline
- Each activity (task) execution with inputs, outputs, and timing
- Error details for failed activities
-
Activity failures: Click a failed activity to see:
- The error message and stack trace
- Input arguments that caused the failure
- Retry history
Debugging a failed workflow
- Find the workflow by ID (shown in the job detail page or logged by the worker).
- Open the workflow in the Temporal UI.
- Look at the event history for
ActivityTaskFailedevents. - Inspect the
failurefield for the error message and stack trace. - Check the activity input to understand what data was passed.
Common Pitfalls
Angle units are radians
All rotation gates (rx, ry, rz) use radians, not degrees. A common mistake:
# Wrong: 90 degrees
circuit.rx(90, 0)
# Correct: pi/2 radians
import math
circuit.rx(math.pi / 2, 0)Qubit ordering varies by backend
Different backends use different qubit ordering conventions:
| System | Convention | Qubit 0 |
|---|---|---|
| QASM / Qiskit | Big-endian | Most significant bit |
| qulacs-wasm (browser) | Little-endian | Least significant bit |
| Braket | Big-endian | Most significant bit |
When using the browser simulator with qulacs-wasm, bitstrings in measurement results are reversed compared to QASM/Qiskit convention. The platform handles this conversion automatically, but be aware of it when comparing results manually.
Measurement is implicit in Braket
When using MarqovDevice.run() with a Braket backend, all qubits are automatically measured. You do not need to add explicit measurement operations to your circuit. For Azure backends, measure_all() is added automatically if no classical registers exist.
cloudpickle version mismatch
Tasks are serialized using cloudpickle. If the cloudpickle version differs between the dispatch environment and the worker environment, deserialization may fail with:
_pickle.UnpicklingError: invalid load keyEnsure the same cloudpickle version is installed in both environments.
Large circuit serialization
Very large circuits (thousands of gates) may produce large serialized payloads that exceed Temporal’s default payload size limit (2 MB). For large circuits:
- Use
circuit.to_openqasm()instead ofcircuit.to_dict()for more compact serialization. - Or increase Temporal’s payload size limit via dynamic configuration.
Direct vs Temporal Execution
| Mode | execution_mode | When to use |
|---|---|---|
| Direct | "direct" | Simple, single-task jobs. Faster startup. Default mode. |
| Temporal | "temporal" | Multi-task workflows, jobs needing retry/durability, long-running jobs |
Direct mode executes the script synchronously in the worker process. Temporal mode dispatches a workflow that executes tasks as Temporal activities with parallelization and retry support.