Functionals
The functionals module provides high-level quantum execution procedures by combining tools from the
analog, digital, and common modules. Currently, it includes two primitive functionals:
Sampling— Executes repeated sampling of a digital quantum circuit.TimeEvolution— Simulates analog time evolution of one or more Hamiltonians according to a time-dependent schedule.
Moreover, it provides more complex functionals that are used to execute more complex algorithms:
VariationalProgram— Builds parameterized program to be optimized in a hybrid quantum-classical environment.
Architecture Overview
Every functional conforms to the abstract Functional interface. Primitive
functionals such as Sampling and
TimeEvolution also inherit from
PrimitiveFunctional, which mixes in the
Parameterizable contract. This lets backends query and update symbolic
parameters consistently before execution.
Each functional advertises a matching result_type pointing to a concrete
FunctionalResult subclass. When you call
execute(), the backend inspects this attribute to decide which result object to
construct and return. Variational workflows reuse this mechanism: optimize()
drives a VariationalProgram by repeatedly updating the nested
functional’s parameters and finally wrapping the optimizer report inside a
VariationalProgramResult.
Result Objects
SamplingResultstores shot counts, probabilities and convenience helpers such as
get_probabilities().
TimeEvolutionResultcontains expectation values, the terminal
QTensorstate, and (optionally) the list of intermediate states whenstore_intermediate_results=True.
VariationalProgramResultbundles the optimizer trajectory (optimal cost, parameters, intermediate steps) together with the functional result obtained at convergence.
These objects make post-processing workflows ergonomic. For example, SamplingResult can surface the most likely
bitstrings:
from qilisdk.backends import QutipBackend
from qilisdk.functionals import Sampling
backend = QutipBackend()
sampling_result = backend.execute(Sampling(circuit, nshots=1_000))
top = sampling_result.get_probabilities(5)
print("Most likely outcomes:", top)
Sampling
The Sampling functional runs a digital quantum circuit multiple times (shots) and aggregates the measurement outcomes. Because it subclasses
PrimitiveFunctional, any symbolic parameters exposed by the underlying
Circuit can be queried or updated through helper methods such as
get_parameter_names().
Parameters
circuit (
Circuit): Circuit to be sampled.nshots (int): Number of times to execute the circuit and collect measurement results.
Returns
SamplingResult: Access shot counts viasamples, or probabilities throughprobabilities. Convenience helpers likeget_probability()ease downstream analyses.
Usage Example
import numpy as np
from qilisdk.digital import Circuit, H, RX, CNOT
from qilisdk.functionals import Sampling
# Create a 2‑qubit circuit
circuit = Circuit(2)
circuit.add(H(0))
circuit.add(RX(0, theta=np.pi))
circuit.add(CNOT(0, 1))
# Initialize the Sampling functional with 100 shots
sampling = Sampling(circuit=circuit, nshots=100)
This functional can be executed on any backend that supports digital circuits. For we can execute it on the CUDA backend:
from qilisdk.backends import CudaBackend
# Run on Qutip backend and retrieve counts
backend = CudaBackend()
results = backend.execute(sampling)
print(results)
Output
SamplingResult(
nshots=100,
samples={'00': 53, '11': 47}
)
Time Evolution
The TimeEvolution functional simulates analog evolution of a quantum system under one or more Hamiltonians, following a specified time‑dependent schedule. Its parameter interface mirrors that of
Schedule, making it straightforward to sweep control waveforms or pulse amplitudes from
classical optimizers.
Parameters
schedule (
Schedule): Defines total evolution time, time steps, Hamiltonians, and their time‑dependent coefficients.initial_state (
QTensor): Initial state of the system.observables (List[
HamiltonianorPauliOperator]): Operators to measure after evolution.nshots (int, optional): Number of repetitions for each observable measurement. Default is 1.
store_intermediate_results (bool, optional): If True, records the state at each time step. Default is False.
Returns
TimeEvolutionResult: Inspectfinal_expected_valuesfor measured observables,final_statefor the closing state, andintermediate_stateswhenstore_intermediate_resultsis enabled.
Usage Example
import numpy as np
from qilisdk.analog import Schedule, X, Z, Y
from qilisdk.common import ket, tensor_prod
from qilisdk.backends import QutipBackend, CudaBackend
from qilisdk.functionals import TimeEvolution
# Define total time and timestep
T = 10.0
dt = 0.1
steps = np.linspace(0, T + dt, int(T / dt))
nqubits = 1
# Define Hamiltonians
Hx = sum(X(i) for i in range(nqubits))
Hz = sum(Z(i) for i in range(nqubits))
# Build a time‑dependent schedule
schedule = Schedule(T, dt)
# Add hx with a time‐dependent coefficient function
schedule.add_hamiltonian(label="hx", hamiltonian=Hx, schedule=lambda t: 1 - steps[t] / T)
# Add hz similarly
schedule.add_hamiltonian(label="hz", hamiltonian=Hz, schedule=lambda t: steps[t] / T)
# Prepare an equal superposition initial state
initial_state = tensor_prod([(ket(0) - ket(1)).unit() for _ in range(nqubits)]).unit()
# Create the TimeEvolution functional
time_evolution = TimeEvolution(
schedule=schedule,
initial_state=initial_state,
observables=[Z(0), X(0), Y(0)],
nshots=100,
store_intermediate_results=False,
)
we can execute it on the Qutip backend:
# Execute on Qutip backend and inspect results
backend = QutipBackend()
results = backend.execute(time_evolution)
print(results)
Output
TimeEvolutionResult(
final_expected_values=array([-0.99388223, 0.0467696 , -0.10005353]),
final_state=QTensor(shape=2x1, nnz=2, format='csr')
[[0.05506547-0.00516502j]
[0.3364973 -0.94005887j]]
)
Variational Programs
The VariationalProgram functional gathers the pieces required for a
variational quantum algorithm. It accepts a parameterized primitive functional, an optimizer, and a cost function. When
you call optimize(), the backend reuses the existing
execute() workflow: it evaluates the functional repeatedly with updated
parameters, feeds the resulting FunctionalResult into the supplied
cost function, and finally returns a VariationalProgramResult.
Parameters
functional (
PrimitiveFunctional): Parameterized primitive to optimize (for instanceSamplingorTimeEvolution).optimizer (
Optimizer): Classical optimizer that proposes new parameter values and optionally stores intermediate iterates.cost_model (
CostFunction): Object that maps the functional results to a scalar cost; frequently constructed from aModel.store_intermediate_results (bool, optional): When True, the optimizer keeps the intermediate steps, which are exposed through
intermediate_results.
Returns
VariationalProgramResult: Retrieveoptimal_cost,optimal_parameters, and the final functional output packaged inoptimal_execution_results.
Usage Example (Using QuTiP Backend)
import numpy as np
from qilisdk.backends import QutipBackend
from qilisdk.common.model import Model, ObjectiveSense
from qilisdk.common.variables import LEQ, BinaryVariable
from qilisdk.cost_functions.model_cost_function import ModelCostFunction
from qilisdk.digital import CNOT, U3, HardwareEfficientAnsatz
from qilisdk.functionals import Sampling
from qilisdk.functionals.variational_program import VariationalProgram
from qilisdk.optimizers.scipy_optimizer import SciPyOptimizer
values = [2, 3, 7]
weights = [1, 3, 3]
max_weight = 4
binary_var = [BinaryVariable(f"b{i}") for i in range(len(values))]
model = Model("Knapsack")
model.set_objective(sum(binary_var[i] * values[i] for i in range(len(values))), sense=ObjectiveSense.MAXIMIZE)
model.add_constraint("max_weights", LEQ(sum(binary_var[i] * weights[i] for i in range(len(weights))), max_weight))
ansatz = HardwareEfficientAnsatz(
nqubits=3, layers=4, connectivity="Circular", one_qubit_gate=U3, two_qubit_gate=CNOT, structure="Interposed"
)
optimizer = SciPyOptimizer(method="Powell")
backend = QutipBackend()
result = backend.execute(VariationalProgram(functional=Sampling(ansatz), optimizer=optimizer, cost_function=ModelCostFunction(model)))
print(result)
Output
VariationalProgramResult(
Optimal Cost = -6.9990000000000006,
Optimal Parameters=[-5.967478124043245e+29,
-2.195285764804736e+29,
2.786404500042059e+29,
7.058510522162697e+29,
2.723590483069596e+29,
1.3020689810431641e+29,
2.322337737413796e+29,
-2.4708791392079937e+29,
-2.9921111116213366e+29,
-4.422367918326788e+29,
-2.2221469613956625e+29,
-2.1656637161893554e+29,
-7.161057019144762e+28,
1.8287982773997508e+29,
-5.8803987767435576e+29,
6.184752232912928e+29,
-3.8100684690039315e+27,
2.258471865930891e+29,
-2.8333713288649098e+29,
-7.432416000178698e+29,
-2.6217720759389174e+29,
-4.081054373720909e+29,
-2.504065530787786e+29,
2.347699364445592e+29,
-5.91304449036894e+29,
-2.3222922004879052e+29,
1.7101847376093354e+29,
2.2630872188582105e+29,
5.819576566857851e+29,
-6.656314000883147e+29,
5.305199782465454e+29,
-2.360680145241282e+29,
-5.953809337209532e+29,
2.3303904290091433e+29,
-5.278640450004206e+29,
-5.278640450004205e+29,
5.291325944436631e+29,
-4.192175488922518e+29,
-2.824792074552054e+29,
4.3280821346328155e+29,
2.3585405087906886e+29,
-3.6102144368881146e+29,
2.3606766206075072e+29,
7.523751475667946e+29,
-5.278307260543703e+29],
Intermediate Results=[])
Optimal results=SamplingResult(
nshots=1000,
samples={'000': 19,
'001': 6,
'010': 42,
'011': 7,
'100': 36,
'101': 848,
'110': 26,
'111': 16}
))