QuOp Functions
- QuOp Function
QuOp Functions define the various aspects of a QVA or modify the simulation methods used by the
quop_mpi.Ansatzclass.Implementation patterns
QuOp Functions can be implemented in three ways, depending on whether you need to maintain state between calls:
Plain function — simplest, for stateless computations:
def my_observables(local_i, local_i_offset, scale, *args, **kwargs): """Compute observables from scratch each time.""" result = np.zeros(local_i, dtype=np.float64) for j in range(local_i): result[j] = compute_cost(local_i_offset + j) * scale return result # Usage: ansatz.set_observables(my_observables, {"args": [2.0]})
Factory function (closure) — for caching or stateful behaviour:
def create_my_function(config_param: float): """Factory returning a stateful QuOp Function.""" _cache = {} # state persists across calls def my_function(local_i, local_i_offset, *args, **kwargs): # config_param captured from enclosing scope # _cache persists between calls (e.g., for expensive one-time setup) if "data" not in _cache: _cache["data"] = expensive_computation() # ... use _cache["data"] ... return result return my_function # Usage: ansatz.set_observables(create_my_function(scale=2.0), obs_dict)
Callable class — for complex state or easier debugging:
class MyFunction: def __init__(self, config_param: float): self.config_param = config_param self._cache = None # state as instance attributes def __call__(self, local_i, local_i_offset, *args, **kwargs): if self._cache is None: self._cache = expensive_computation() # ... implementation ... return result # Usage: ansatz.set_observables(MyFunction(scale=2.0), obs_dict)
Use plain functions when no state is needed. Use the factory/closure pattern when you need to cache expensive computations (e.g., computing global statistics via MPI) or carry configuration. Use callable classes when you need subclassing, multiple methods, or easier state inspection for debugging.
- FunctionDict
Prior to QVA simulation, positional arguments of a QuOp Function are bound to the attributes of the receiving class if a match is found. Additional positional and keyword arguments are specified via a FunctionDict:
function_dict : {"args":List[Any], "kwargs":Dict}
The
"args"and"kwargs"elements of a FunctionDict are both optional. If present, these are passed to a bound QuOp Function as:bound_quop_function(*function_dict["args"], **function_dict["kwargs"])
Bindable Attributes
When defining a QuOp Function, positional parameters are automatically bound
to class attributes by matching the parameter name to the attribute name.
This is the key mechanism: if your function has a parameter named local_i,
it will automatically receive the value of that attribute.
Binding sources differ by function type:
Ansatz-level functions (Observables, Initial State, Parameter Map, Sampling, Objective): bind to
quop_mpi.AnsatzattributesUnitary-level functions (Operator, Parameter): bind to
quop_mpi.Unitaryattributes
Many attributes (like local_i, system_size, MPI_COMM) are shared
between Ansatz and Unitary, so they work in both contexts. However, some
attributes are specific to each class.
Ansatz Bindable Attributes
These attributes are available for Observables, Initial State, Parameter Map, Sampling, and Objective Functions:
Parameter Name |
Type |
Description |
|---|---|---|
|
int |
Total number of quantum basis states |
|
int |
Number of elements in this MPI rank’s partition |
|
int |
Global index offset for this rank’s partition |
|
ndarray[int] |
Array where |
|
ndarray[float64] |
Local partition of observable values (available after setup) |
|
ndarray[complex128] |
Local partition of the initial state vector |
|
ndarray[complex128] |
Local partition of current/final state vector |
|
ndarray[float64] |
Current variational parameter values |
|
int |
Number of ansatz iterations (layers) |
|
int |
Number of variational parameters per ansatz iteration |
|
MPI.Intracomm |
MPI subcommunicator for this Ansatz instance |
|
float |
Last computed objective function value |
|
int |
Random seed for parameter generation |
Important: Parameter names in your function signature must exactly match
the attribute names above to be bound. Parameters that don’t match will need
to be provided via FunctionDict["args"].
Naming convention for custom parameters: To avoid unintended binding, we recommend prefixing your custom parameter names with an underscore:
def my_observables(
local_i, # bound from Ansatz
local_i_offset, # bound from Ansatz
_n_customers, # custom - passed via FunctionDict["args"]
_penalty_weight, # custom - passed via FunctionDict["args"]
):
...
# Usage:
ansatz.set_observables(my_observables, {"args": [10, 0.5]})
This prevents accidental collisions with current or future Ansatz attributes
(e.g., seed, expectation).
Runtime discovery: Use these methods to discover available bindings:
ansatz.print_bindable_attributes()— show Ansatz attributes onlyansatz.print_all_bindable_attributes()— show Ansatz AND all Unitary attributesunitary.print_bindable_attributes()— show attributes for a specific Unitary
For programmatic access, use get_bindable_attributes() which returns a dictionary.
Extensibility: Subclasses (algorithms, propagators) can extend the available
bindable attributes by defining their own BINDABLE_ATTRIBUTES class variable.
The discovery methods automatically collect attributes from the entire class
hierarchy.
Unitary Bindable Attributes
These attributes are available for Operator and Parameter Functions. They are
bound from the quop_mpi.Unitary instance:
Parameter Name |
Type |
Description |
|---|---|---|
|
int |
Total number of quantum basis states (shared with Ansatz) |
|
int |
Number of elements in this MPI rank’s partition (shared) |
|
int |
Global index offset for this rank’s partition (shared) |
|
ndarray[int] |
Array describing global partitioning (shared) |
|
MPI.Intracomm |
MPI subcommunicator (shared) |
|
int |
Random seed for parameter generation (shared) |
|
int |
Size of the operator array (equals |
|
int |
Lower global index of the local partition |
|
int |
Upper global index of the local partition |
|
int |
Total parameters for this Unitary (operator + unitary params) |
|
int |
Number of operator variational parameters |
|
int |
Number of unitary variational parameters |
|
ndarray[float64] |
Operator variational parameters (only for parameterised operators) |
|
ndarray[complex128] |
Local partition of input state to this unitary |
|
ndarray[complex128] |
Local partition of output state from this unitary |
Note
Attributes marked “(shared)” have the same values in both Ansatz and
Unitary contexts. Unitary-specific attributes like variational_parameters
are only meaningful for Operator Functions that define parameterised operators.
- Observables Function
Returns a 1-D real array containing
local_ielements of the observables with global offsetlocal_i_offset. Passed to thequop_mpi.Ansatz.set_observables()method and bound to the attributes of thequop_mpi.Ansatzclass.Predefined Observables Functions are included in the
quop_mpi.observablemodule.Commonly used parameters:
local_i— number of observables this rank must computelocal_i_offset— starting global index for this ranksystem_size— total number of basis statespartition_table— for advanced partitioning schemes
Typical structure:
def observables_function( local_i : int, local_i_offset : int, *args, **kwargs) -> np.ndarray[np.float64]: ... return observables
- Initial State Function
Returns a 1-D complex array containing
local_ielements of the initial state with global offsetlocal_i_offset. Passed to thequop_mpi.Ansatz.set_initial_state()method and bound to the attributes of thequop_mpi.Ansatzclass.Predefined Initial State Functions are included in the
quop_mpi.statemodule.Commonly used parameters:
local_i— number of state elements this rank must computelocal_i_offset— starting global index for this ranksystem_size— total number of basis states
Typical structure:
def initial_state_function( local_i : int, local_i_offset : int, system_size : int, *args, **kwargs) -> np.ndarray[np.complex128]: ... return initial_state
- Parameter Map Function
Defines a mapping from a reduced “free” parameter vector to the full variational-parameter vector used by a QVA. This allows you to optimise over a smaller parameter space while the mapping function reconstructs the complete vector internally.
Passed to
quop_mpi.Ansatz.set_parameter_map()together with the number of free parameters and an optional FunctionDict.Method signature:
ansatz.set_parameter_map( n_free_params, # int: dimensionality of the optimisation problem mapping_fn, # callable: your mapping function mapping_dict # optional FunctionDict for extra arguments )
Parameters:
The first positional parameter always receives the free parameter vector from the optimiser. Additional parameters depend on your mapping logic:
ansatz_depth— number of ansatz iterations (for computing output size)total_params— parameters per iteration (for computing output size)observables— for normalising parameters by observable statisticsMPI_COMM— for computing global statistics across ranks
Typical structure:
def mapping_fn( free_vec: np.ndarray, ansatz_depth: int, total_params: int, *args, **kwargs ) -> np.ndarray: """ Map free_vec to full parameter vector of shape (ansatz_depth * total_params,). """ full_vec = np.zeros(ansatz_depth * total_params, dtype=np.float64) # ... your mapping logic ... return full_vec
Factory pattern example:
For Parameter Map Functions, the factory pattern conveniently returns both
n_free_paramsand the mapping function together:def create_linear_schedule(scale: float): """Factory returning (n_free_params, mapping_fn).""" n_free_params = 3 _cache = {} def mapping_fn(free_vec, ansatz_depth, observables, MPI_COMM): if "sigma" not in _cache: _cache["sigma"] = compute_global_std(observables, MPI_COMM) # ... build full_vec from free_vec ... return full_vec return n_free_params, mapping_fn # Usage: n_free, param_map = create_linear_schedule(scale=1.0) ansatz.set_parameter_map(n_free, param_map)
See QuOp Function for the general implementation patterns (factory/closure vs callable class).
- Sampling Function
Returns an objective function value computed from batches of observables values that are sampled based on the probability distribution of the wavefunction state vector during simulation together with a boolean that specifies whether the objective function value should be passed to the optimiser or more sample batches taken. Passed to
quop_mpi.Ansatz.set_sampling().See
quop_mpi.Ansatzfor a selected list of available attributes,Note
The
quop_mpi.Ansatzclass computes the expectation value exactly by default.Typical Structure
def sampling_function( samples : List[ndarray[float64]], *args, **kwargs ) -> (float, bool) ... return objective_function_value, value_accepted
The
samplesargument is a list of 1-D real arrays containingsample_block_sizeobservables values. Ifvalue_acceptedis notTrue, an additional sample block is appended tosamples.- Jacobian Function
Enables distributed parallel computation of the objective function gradient. Returns the partial derivative of the objective function with respect to the variational parameter with index
var. Used to compute the objective function gradient is parallel if using a gradient-informed optimiser. Passed toquop_mpi.Ansatz.set_parallel_jacobian().The
quop_mpi.Ansatzsupports numerical approximation of the gradient using the forward and central finite difference methods (specified viaquop_mpi.Ansatz.set_parallel_jacobian()). Seequop_mpi.Ansatzfor a list of available attributes.Note
The
quop_mpi.Ansatzclass computes the objective function gradient sequentially by default.The default optimisation method of the
quop_mpi.Ansatzclass, the BFGS algorithm, is gradient informed.
Typical Structure
def jacobian_function( variational_parameters: np.ndarray[np.float], evaluate: Callable, var: int, *args, **kwargs ) -> float: ... return partial_derivative_value
The
evaluateargument is bound to thequop_mpi.Ansatz.evaluate()method which implements lazy computation of the objective function. This is the recommended method for use in numerical approximation of the gradient by finite-difference methods.- Operator Function
Returns an operator object that is compatible with the propagation method of a specific
unitaryclass. Seequop_mpi.Unitary.Note
Operator Functions bind to Unitary attributes, not Ansatz attributes. See the “Unitary Bindable Attributes” table above.
Predefined Operator Functions are included with each
unitaryclass in thequop_mpi.propagatormodule underquop_mpi.propagator.<unitary>.operator.Commonly used parameters (bound from Unitary):
local_i— partition size for this ranklocal_i_offset— global index offsetsystem_size— total number of basis statesvariational_parameters— only if the operator is parameterised
Typical Structure
def operator_function( local_i : int, local_i_offset : int, *args, **kwargs ) -> Any: ... return operator
Operator Functions with one or more variational parameters require the
variational_parameterspositional argument. Operator Functions with no variational parameters do not.- Parameter Function
Returns initial values for the variational parameters associated with an instance of the
quop_mpi.Unitaryclass.Note
Parameter Functions bind to Unitary attributes, not Ansatz attributes. See the “Unitary Bindable Attributes” table above.
Predefined Parameter Functions are included in the
quop_mpi.parammodule.Commonly used parameters (bound from Unitary):
n_params— number of parameters to generate
Typical Structure
def param_function( n_params : int, *args, **kwargs ) -> List[float]: ... return variational_parameters
- Objective Function
Called after state-evolution during parameter optimisation. Returns a scalar value for minimisation. Passed to
quop_mpi.Ansatz.set_objective().Commonly used parameters:
local_probabilities— probability amplitudes for this rank’s partitionobservables— observable values for this rank’s partitionMPI_COMM— MPI subcommunicator (for global reductions, e.g., CVaR)
Typical Structure
def objective_function( local_probabilities: np.ndarray[np.float64], observables: np.ndarray[np.float64], MPI_COMM: MPI.Intracomm, *args, **kwargs ) -> float: ... return objective_function_value