Evaluation¶
Some applications (e.g. variational quantum algorithms such as QAOA) require computing expectation values of operators for certain states. ParityOS offers a very flexible framework for defining a wide variety of evaluation schemes for computing expectation values or various operator-like objects in the module parityos.evaluators.
This framework heavily relies on composition over inheritance to allow for maximum flexibility and customizability.
Abstract Base Classes¶
ParityOS defines the following base classes for expectation value computation in parityos.evaluators.evaluator
OperatorEvaluator: A class for evaluatingOperatorinstances for a generalState.ConstraintEvaluator: A class for evaluatingConstraintinstances (e.g.ProductConstraintorSumConstraint) for a generalState.ProblemEvaluator: A class for evaluatingProblemRepresentationinstances for a generalState.
The classes are built hierarchically, where top level classes take instances of low level classes. E.g. a ConstraintEvaluator needs an OperatorEvaluator, while a ProblemEvaluator needs both a ConstraintEvaluator and an OperatorEvaluator.
The abstract base classes do not supply __init__ methods for flexibility and customizability reasons. It is advisable that implementing classes take instances of lower level evaluator instances at construction, following the composition paradigm.
For a concrete reference implementation of these classes for PauliOperator and PauliBasisState instances, see module parityos.evaluators.pauli_evaluator.
Operator Evaluator¶
The OperatorEvaluator class is the basic building block of the evaluator hierarchy and supplies the abstract method evaluate_operator that takes a State and an Operator instance and returns its expectation value.
Based on this abstract method, the class also supplies two default implementations of
evaluate_operator_product: evaluates anOperatorProductfor aStateevaluate_operator_polynomial: evaluates anOperatorPolynomialfor aState
Both default implementations are trivial and utilize evaluate_operator internally. They can be overridden with potentially more efficient implementations.
Constraint Evaluator¶
The ConstraintEvaluator class is responsible for evaluating constraints, including penalty schemes for states that violate a given constraint.
It supplies the abstract evaluate_constraints method that takes a state and an iterable of constrains and that must implement aggregate constraint evaluation including violation penalization.
Implementing classes may take an OperatorEvaluator object at construction to help with constraint evaluation.
Problem Evaluator¶
The ProblemEvaluator evaluates a problem Hamiltonian and a set of constraints in form of a
ProblemRepresentation for a given State.
It supplies the abstract evaluate_problem method, responsible for evaluating the problem Hamiltonian and aggregating the evaluations of all problem constraints.
Implementing classes may take an OperatorEvaluator and a ConstraintEvaluator object at construction to help with problem evaluation.
Pauli Evaluator¶
ParityOS supplies a built-in implementation of the above evaluator classes for PauliOperator and PauliBasisState instances in the module parityos.evaluators.pauli_evaluator.
Pauli evaluators are the most simple evaluators in the sense that they compute expectation values of operators under states which are required to be eigenstates of the operator. The computations therefore reduce to simple manipulations of the Pauli operator eigenvalues \(\pm 1\).
Warning
States and operators in Pauli evaluators must be in the same basis. Compounds such as OperatorProduct and OperatorPolynomial can only contain operators of the same type as the state’s basis.
For more information on Pauli basis states, refer also to Pauli States.
Pauli Operator Evaluator¶
The PauliOperatorEvaluator class takes no arguments and supplies a simple implementation of evaluate_operator
from parityos.bits import Qubit
from parityos.evaluators import PauliOperatorEvaluator
from parityos.operators.elementary_operator import X, Z
from parityos.states.pauli_state import PauliBasisState
from parityos.utils.exceptions import ParityOSTypeError
evaluator = PauliOperatorEvaluator()
q0, q1 = (Qubit(0), Qubit(1))
z0, z1 = Z(q0), Z(q1)
state_z = PauliBasisState("10", (q0, q1), Z)
assert evaluator.evaluate_operator(state_z, z0) == -1
assert evaluator.evaluate_operator(state_z, z1) == 1
assert evaluator.evaluate_operator_product(state_z, z0 * z1) == -1
assert evaluator.evaluate_operator_polynomial(state_z, z0 - 2 * z0 * z1) == 1
try:
# this raises ParityOSTypeError since X is not the same as `state_z`'s basis (Z)
evaluator.evaluate_operator(state_z, X(q0))
raise AssertionError()
except ParityOSTypeError:
pass
try:
# this raises ParityOSTypeError since the polynomial contains X,
# which is not the same as `state_z`'s basis (Z)
evaluator.evaluate_operator_polynomial(state_z, X(q0) - z0)
raise AssertionError()
except ParityOSTypeError:
pass
Pauli Constraint Evaluator¶
ParityOS provides two types of ConstraintEvaluator implementations for Pauli operators and basis states:
HardConstraintEvaluator: Immediately returns infinity (float("inf")) if one of the passed constraints is violated.SoftConstraintEvaluator: Counts the number of violated constraints and multiplies this number by a givenconstraint_strength
As both evaluators only count violated constraints using Constraint.is_satisfied, they don’t need a PauliOperatorEvaluator.
from parityos.bits import Qubit
from parityos.evaluators import (
HardConstraintEvaluator,
SoftConstraintEvaluator,
)
from parityos.operators.elementary_operator import X, Z
from parityos.optimization.constraint import ProductConstraint, SumConstraint
from parityos.states.pauli_state import PauliBasisState
from parityos.utils.constants import INFINITY
from parityos.utils.exceptions import ParityOSTypeError
hard_evaluator = HardConstraintEvaluator()
q0, q1, q2 = Qubit(0), Qubit(1), Qubit(2)
z0, z1, z2 = Z(q0), Z(q1), Z(q2)
state_z = PauliBasisState("101", (q0, q1, q2), Z)
state_x = PauliBasisState("11", (q0, q1), X)
# these constraints are fulfilled
product_02 = ProductConstraint(z0 * z2)
sum_012_2 = SumConstraint(z0 * z1 + z1 * z2, -2)
# these constraints are violated
product_01 = ProductConstraint(z0 * z1)
sum_012_0 = SumConstraint(z0 * z1 + z1 * z2)
# HARD CONSTRAINT EVALUATION
assert hard_evaluator.evaluate_constraints(state_z, [product_02]) == 0
assert hard_evaluator.evaluate_constraints(state_z, [product_02, sum_012_2]) == 0
# constraint is violated
assert hard_evaluator.evaluate_constraints(state_z, [product_01]) == INFINITY
# second constraint is violated
assert hard_evaluator.evaluate_constraints(state_z, [product_02, sum_012_0]) == INFINITY
try:
# this raises ParityOSTypeError the state is in X basis
hard_evaluator.evaluate_constraints(state_x, [product_01]) # pyright: ignore[reportArgumentType]
raise AssertionError()
except ParityOSTypeError:
pass
# SOFT CONSTRAINTS
soft_evaluator = SoftConstraintEvaluator(2) # constraint strength 2
assert soft_evaluator.evaluate_constraints(state_z, [product_02]) == 0
assert soft_evaluator.evaluate_constraints(state_z, [product_02, sum_012_2]) == 0
# constraint is violated
assert soft_evaluator.evaluate_constraints(state_z, [product_01]) == 2
# both constraints are violated
assert soft_evaluator.evaluate_constraints(state_z, [product_01, sum_012_0]) == 4
try:
# this raises ParityOSTypeError as the state is in the X basis
soft_evaluator.evaluate_constraints(state_x, [product_01]) # pyright: ignore[reportArgumentType]
raise AssertionError()
except ParityOSTypeError:
pass
Pauli Problem Evaluator¶
ParityOS supplies two types of ProblemEvaluator implementations for Pauli operators and basis states:
SingleProblemEvaluator: Evaluates aProblemRepresentationfor a singlePauliBasisState. Takes and uses anOperatorEvaluatorand aConstraintEvaluatorobject.AverageProblemEvaluator: Evaluates aProblemRepresentationfor a statistical ensemble ofPauliBasisStates[1] and returns the probability weighted average of all evaluations for eachPauliBasisStatecontained in the state. Takes and uses aProblemEvaluatorto evaluate each single basis state contribution.MinimalProblemEvaluator: LikeAverageProblemEvaluator, but returns the minimum of all evaluations for eachPauliBasisStatecontained in the state.
The following example also demonstrates the power of composition, where inheritance would require an exponentially growing number of subclasses with unavoidable code duplication.
from parityos.bits import Qubit
from parityos.evaluators.pauli_evaluator import (
AverageProblemEvaluator,
HardConstraintEvaluator,
MinimalProblemEvaluator,
PauliOperatorEvaluator,
SingleProblemEvaluator,
SoftConstraintEvaluator,
)
from parityos.operators.elementary_operator import X, Z
from parityos.optimization.constraint import ProductConstraint, SumConstraint
from parityos.optimization.problem_representation import ProblemRepresentation
from parityos.states import PauliBasisStateCounts, PauliQuantumState
from parityos.states.pauli_state import PauliBasisState
from parityos.utils.constants import INFINITY
from parityos.utils.exceptions import ParityOSTypeError
operator_evaluator = PauliOperatorEvaluator()
hard_constraint_evaluator = HardConstraintEvaluator()
soft_constraint_evaluator = SoftConstraintEvaluator(2)
q0, q1, q2 = Qubit(0), Qubit(1), Qubit(2)
z0, z1, z2 = Z(q0), Z(q1), Z(q2)
problem = ProblemRepresentation(
z0 + z1 - z2, ProductConstraint(z0 * z2), SumConstraint(z0 * z1 + z1 * z2, -2)
)
# fulfills all constraints, Hamiltonian evaluates to 1
state_101 = PauliBasisState("101")
# fulfills all constraints, Hamiltonian evaluates to -1
state_010 = PauliBasisState("010")
# violates sum constraint, Hamiltonian evaluates to -1
state_111 = PauliBasisState("111")
# SINGLE STATE EVALUATION
single_hard_evaluator = SingleProblemEvaluator(operator_evaluator, hard_constraint_evaluator)
single_soft_evaluator = SingleProblemEvaluator(operator_evaluator, soft_constraint_evaluator)
assert single_hard_evaluator.evaluate_problem(state_101, problem) == 1
assert single_soft_evaluator.evaluate_problem(state_101, problem) == 1
assert single_hard_evaluator.evaluate_problem(state_010, problem) == -1
assert single_soft_evaluator.evaluate_problem(state_010, problem) == -1
assert single_hard_evaluator.evaluate_problem(state_111, problem) == INFINITY
# Here the Hamiltonian evaluates to -1, but constraint violation contributes +2
assert single_soft_evaluator.evaluate_problem(state_111, problem) == 1
try:
state_x = PauliBasisState("101", basis=X)
# this raises ParityOSTypeError as the state is in the X basis
single_hard_evaluator.evaluate_problem(state_x, problem) # pyright: ignore[reportArgumentType]
raise AssertionError()
except ParityOSTypeError:
pass
# STATE ENSEMBLE EVALUATION
avg_hard_evaluator = AverageProblemEvaluator(single_hard_evaluator)
avg_soft_evaluator = AverageProblemEvaluator(single_soft_evaluator)
min_hard_evaluator = MinimalProblemEvaluator(single_hard_evaluator)
min_soft_evaluator = MinimalProblemEvaluator(single_soft_evaluator)
# both basis states fulfill all constraints
counts = PauliBasisStateCounts.from_state_to_count({state_101: 3, state_010: 1})
state = PauliQuantumState.from_state_to_amplitude({state_101: -3, state_010: 1j})
assert avg_hard_evaluator.evaluate_problem(counts, problem) == 0.5
assert avg_soft_evaluator.evaluate_problem(counts, problem) == 0.5
assert avg_hard_evaluator.evaluate_problem(state, problem) == 0.8
assert avg_soft_evaluator.evaluate_problem(state, problem) == 0.8
assert min_hard_evaluator.evaluate_problem(counts, problem) == -1
assert min_soft_evaluator.evaluate_problem(counts, problem) == -1
assert min_hard_evaluator.evaluate_problem(state, problem) == -1
assert min_soft_evaluator.evaluate_problem(state, problem) == -1
# second basis state violates sum constraint
counts = PauliBasisStateCounts.from_state_to_count({state_101: 3, state_111: 1})
state = PauliQuantumState.from_state_to_amplitude({state_101: -3, state_111: 1j})
assert avg_hard_evaluator.evaluate_problem(counts, problem) == INFINITY
assert avg_soft_evaluator.evaluate_problem(counts, problem) == 1
assert avg_hard_evaluator.evaluate_problem(state, problem) == INFINITY
assert avg_soft_evaluator.evaluate_problem(state, problem) == 1
# picks the contribution of `state_101`, which fulfills the constraint
assert min_hard_evaluator.evaluate_problem(counts, problem) == 1
assert min_soft_evaluator.evaluate_problem(counts, problem) == 1
assert min_hard_evaluator.evaluate_problem(state, problem) == 1
assert min_soft_evaluator.evaluate_problem(state, problem) == 1
try:
counts_x = PauliBasisStateCounts({"101": 3, "010": 1}, basis=X)
# this raises ParityOSTypeError as the state is in the X basis
avg_soft_evaluator.evaluate_problem(counts_x, problem) # pyright: ignore[reportArgumentType]
raise AssertionError()
except ParityOSTypeError:
pass