Getting Started with qvivid

Sanmi (Oluwasanmi) Adenaiye

qvivid provides circuit construction, exact statevector simulation, terminal measurement sampling, tidy inspection, and publication-oriented graphics in one R workflow. The simulation core does not require Python. This vignette introduces the workflow with a Bell state and documents the data structures that remain stable in qvivid 0.1.x.

library(qvivid)

Build and simulate a Bell circuit

Gate functions take a circuit as their first argument, so they work naturally with R’s base pipe. Qubit indices start at one.

bell <- quantum_circuit(2, name = "Bell state") |>
  gate_h(1) |>
  gate_cx(control = 1, target = 2) |>
  measure_all()

bell
#> <qv_circuit: Bell state>
#>   2 qubits | 2 classical bits | 3 operations | depth 3
#>    1  H        q1
#>    2  CX       q1,q2
#>    3  M        q1,q2 -> c1,c2

simulate_quantum() always computes the exact final state. Set shots to also sample terminal measurements, seed to make those samples reproducible, and record = TRUE to retain the state after every operation.

result <- simulate_quantum(
  bell,
  shots = 256,
  seed = 2026,
  record = TRUE
)

result
#> <qv_result>
#>   2 qubits | native backend | 0.000000 seconds
#>   Exact state probabilities:
#>     |00>  0.500000
#>     |11>  0.500000
#>   256 shots | seed 2026
#>     00  141
#>     11  115
result$counts
#>   basis count probability
#> 1    00   141   0.5507812
#> 2    11   115   0.4492188

Measurement in qvivid 0.1.x is terminal: it samples the final state but does not perform mid-circuit collapse, reset, or conditional execution. If a circuit has no explicit measurement operation, shot sampling uses the default mapping from each available qubit to the matching classical bit.

Statevector memory grows exponentially with the number of qubits. Recorded trajectories retain an additional state for every unitary operation; terminal measurement frames reuse the preceding state. Before allocating the state, qvivid estimates peak memory and compares it with memory_limit_gib (2 GiB by default, configurable with the qvivid.memory_limit_gib option). Raise that guard only after confirming that the machine has enough memory; use record = FALSE for large circuits.

Qubit and basis ordering

The R API uses one-based qubits. In the statevector, qubit 1 is the least-significant bit (LSB). Displayed basis strings are written from the highest qubit down to qubit 1. Consequently, applying X to qubit 1 in a two-qubit register populates |01>, not |10>.

lsb_result <- quantum_circuit(2, name = "LSB example") |>
  gate_x(1) |>
  simulate_quantum()

state_data(lsb_result, include_zero = FALSE)
#>   index basis real imaginary magnitude probability phase
#> 1     1    01    1         0         1           1     0

For two-qubit custom unitaries, the first qubit supplied to gate_unitary() is the most-significant qubit in the matrix’s local basis order |00>, |01>, |10>, |11>.

Inspect states, counts, and trajectories

Use state_data() instead of reconstructing labels or phases from the raw statevector. Its stable 0.1.x columns are:

Column Meaning
index Zero-based integer statevector index
basis High-to-low binary basis label, ending in qubit 1
real, imaginary Cartesian components of the amplitude
magnitude Absolute value of the amplitude
probability Squared magnitude
phase Complex phase in radians; NA for a numerically zero amplitude
state_data(result, include_zero = FALSE)
#>   index basis      real imaginary magnitude probability phase
#> 1     0    00 0.7071068         0 0.7071068         0.5     0
#> 2     3    11 0.7071068         0 0.7071068         0.5     0

result$counts is always a data frame with basis, count, and probability columns. It has zero rows when shots = NULL. Count labels are classical-bit strings ordered from the highest classical bit down to bit 1; the circuit’s terminal measurement mapping determines which qubit supplies each bit.

When record = TRUE, trajectory_data() returns the state-data columns above plus step and label. Step 0 is the initial state, and each subsequent step corresponds to one circuit operation.

trajectory <- trajectory_data(result, include_zero = FALSE)
trajectory[, c("step", "label", "basis", "probability")]
#>   step   label basis probability
#> 1    0 Initial    00         1.0
#> 2    1       H    00         0.5
#> 3    1       H    01         0.5
#> 4    2      CX    00         0.5
#> 5    2      CX    11         0.5
#> 6    3       M    00         0.5
#> 7    3       M    11         0.5

trajectory_bloch(result, qubit = 1)
#>   step   label qubit n_qubits x y z radius purity
#> 1    0 Initial     1        2 0 0 1      1    1.0
#> 2    1       H     1        2 1 0 0      1    1.0
#> 3    2      CX     1        2 0 0 0      0    0.5
#> 4    3       M     1        2 0 0 0      0    0.5

Stable data structures in qvivid 0.1.x

A qv_result is a named list with class qv_result. The following named fields and their meanings are stable through the 0.1.x series:

Field Meaning
circuit The simulated qv_circuit
state Normalized complex vector of length 2^n in LSB order
probabilities Numeric vector aligned with state
counts The three-column terminal-shot table described above
shots, seed Requested values, or NULL when omitted
backend Backend actually used: "native" or "reference"
elapsed Simulation elapsed time in seconds
trajectory Recorded frames, or NULL when record = FALSE
schema_version Integer version of the result schema; currently 1L

A qv_circuit has stable name, n_qubits, n_clbits, operations, and schema_version fields. Each operation has stable type, name, label, qubits, clbits, matrix, and parameters fields. Use constructors and gate functions to create or modify circuits; treat these named fields as read-only interoperability data.

Each raw result$trajectory frame has stable step, label, operation, and state fields. Prefer trajectory_data() for tabular analysis. Its stable columns are the seven state_data() columns followed by step and label. trajectory_bloch() returns stable step, label, qubit, n_qubits, x, y, z, radius, and purity columns.

Minor 0.1.x releases may append named fields, columns, optional arguments, or new exported functions. They will not remove or rename the fields and columns listed above, change their meaning, or reorder existing tabular columns. Code should select fields and columns by name rather than depend on list position.

Plot and export

plot() dispatches a circuit to plot_circuit() and a result to plot_state(). Explicit plotting functions expose the same visual presets: "nature", "npj", "colorblind", "dark", "light", and "mono". Plot behavior does not change with installed packages: plot_state() uses base graphics by default and returns the plotted state data invisibly. engine = "auto" is a compatibility alias for base graphics. An explicit engine = "ggplot2" requires the suggested ggplot2 package and returns a ggplot object for printing or composition. plot_circuit() returns its circuit invisibly, and plot_execution() returns its result invisibly.

plot(result, theme = "npj", engine = "base")
Exact Bell-state probabilities and phases.
Exact Bell-state probabilities and phases.

Recorded runs can synchronize the circuit playhead and exact state at any execution step.

plot_execution(result, step = 2, theme = "nature")
Circuit and exact state after the controlled-X gate.
Circuit and exact state after the controlled-X gate.

save_quantum_plot() supports PDF, SVG, PNG, and TIFF. Always provide a path you control; package examples and vignettes should write only to R’s temporary directory.

figure_file <- tempfile(fileext = ".pdf")
artifact <- save_quantum_plot(
  result,
  figure_file,
  view = "execution",
  step = 2,
  size = "single",
  theme = "nature"
)

file.exists(artifact$path)
#> [1] TRUE
unlink(artifact$path)

Optional GIF animation

GIF export requires the suggested gifski package. The following chunk is evaluated only when gifski is installed and the environment variable QVIVID_BUILD_GIF=true is set. It is skipped during ordinary package and CRAN vignette builds, and its output is confined to tempdir().

gif_file <- tempfile(fileext = ".gif")
animation <- animate_state(
  result,
  gif_file,
  fps = 2,
  width = 640,
  height = 480,
  theme = "npj",
  progress = FALSE
)

animation
unlink(animation$path)

Compatibility and deprecation policy

The exported functions, required arguments, S3 classes, and data structures documented above will remain compatible throughout qvivid 0.1.x. New optional arguments, functions, fields, and columns may be added. If an interface must be replaced, qvivid will issue a warning for at least one 0.1.x release and keep the old interface available throughout the series. Removal can occur no earlier than 0.2.0. A serious correctness or security defect may require an earlier change; such changes will be recorded in NEWS.md.

Unexported names (including names beginning with .qv_), exact timing values, warning text, and pixel-level plot geometry are implementation details. Plot engine and return-value semantics, theme names, basis ordering, and data meanings remain documented public behavior.