Skip to content

quast_decisiontree.algorithms.hybrid.variational

quast_decisiontree.algorithms.hybrid.variational

VariationalResult dataclass

Result of a variational algorithm execution.

Attributes:

Name Type Description
counts dict[str, int]

Final measurement counts from the optimized circuit.

optimal_params ndarray | None

Optimized parameter array.

cost_history list[float]

List of cost values recorded during optimization.

num_evals int

Total number of cost function evaluations.

Source code in src/quast_decisiontree/algorithms/hybrid/variational.py
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
@dataclass
class VariationalResult:
    """Result of a variational algorithm execution.

    Attributes:
        counts: Final measurement counts from the optimized circuit.
        optimal_params: Optimized parameter array.
        cost_history: List of cost values recorded during optimization.
        num_evals: Total number of cost function evaluations.
    """

    counts: dict[str, int]
    optimal_params: np.ndarray | None = None
    cost_history: list[float] = field(default_factory=list)
    num_evals: int = 0

counts instance-attribute

counts

optimal_params class-attribute instance-attribute

optimal_params = None

cost_history class-attribute instance-attribute

cost_history = field(default_factory=list)

num_evals class-attribute instance-attribute

num_evals = 0

__init__

__init__(
    counts,
    optimal_params=None,
    cost_history=list(),
    num_evals=0,
)

VariationalAlgorithm

Bases: HybridAlgorithm

Template for custom variational quantum-classical optimization loops.

Implements the standard variational pattern
  1. Initialize parameters for the ansatz
  2. Variational loop (driven by optimizer): ansatz(qv, params) → backend.run() → cl_cost_function(counts) → scalar cost
  3. Final measurement with optimized parameters → return VariationalResult

The optimizer (built by OptimizerBuilder) drives the loop via its .minimize() method. The template builds a cost closure that the optimizer calls repeatedly.

Hyperparameters (set by upstream nodes via problem_data): - ansatz: Callable(QuantumVariable, np.ndarray) → None. Applies parameterized gates to a QuantumVariable. Must expose a num_params attribute (int) indicating the parameter count. - optimizer: Optimizer instance (built by OptimizerBuilder). Must have a .minimize(fun, x0, bounds=None) method returning a result with .x. - shots: Number of measurement shots per circuit evaluation. - init_params: Optional initial parameter array for warm-starting. If None, random initialization in [0, 2π) is used.

Input keys (problem-specific data): - cl_cost_function: Callable(Dict[str, int]) → float. Maps measurement counts to a scalar cost value. - num_qubits: int. Number of qubits for the problem.

Source code in src/quast_decisiontree/algorithms/hybrid/variational.py
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
class VariationalAlgorithm(HybridAlgorithm):
    """Template for custom variational quantum-classical optimization loops.

    Implements the standard variational pattern:
        1. Initialize parameters for the ansatz
        2. Variational loop (driven by optimizer):
           ansatz(qv, params) → backend.run() → cl_cost_function(counts) → scalar cost
        3. Final measurement with optimized parameters → return VariationalResult

    The optimizer (built by OptimizerBuilder) drives the loop via its .minimize() method.
    The template builds a cost closure that the optimizer calls repeatedly.

    Hyperparameters (set by upstream nodes via problem_data):
        - ansatz: Callable(QuantumVariable, np.ndarray) → None.
            Applies parameterized gates to a QuantumVariable.
            Must expose a `num_params` attribute (int) indicating the parameter count.
        - optimizer: Optimizer instance (built by OptimizerBuilder).
            Must have a .minimize(fun, x0, bounds=None) method returning a result with .x.
        - shots: Number of measurement shots per circuit evaluation.
        - init_params: Optional initial parameter array for warm-starting.
            If None, random initialization in [0, 2π) is used.

    Input keys (problem-specific data):
        - cl_cost_function: Callable(Dict[str, int]) → float.
            Maps measurement counts to a scalar cost value.
        - num_qubits: int. Number of qubits for the problem.
    """

    HYPERPARAMS: ClassVar[list] = [
        HyperParam(
            name="ansatz",
            hparam_type=None,
            description="Parameterized ansatz: callable(qv, params) with .num_params attribute",
        ),
        HyperParam(
            name="optimizer",
            hparam_type=None,
            description=(
                "Optimizer instance with .minimize(fun, x0, bounds=None) → result with .x. "
                "Built by OptimizerBuilder."
            ),
        ),
        HyperParam(
            name="shots",
            hparam_type=int,
            description="Number of shots per circuit evaluation",
            default=1024,
            test=lambda x: x > 0,
        ),
        HyperParam(
            name="init_params",
            hparam_type=None,
            description="Optional initial parameter array for warm-starting (None = random)",
            default=None,
        ),
    ]

    INPUT_KEYS: ClassVar[Sequence[str]] = (
        "cl_cost_function",
        "num_qubits",
    )

    def reset(self) -> None:
        """Clear input, backend, and algorithm state after execution."""
        super().reset()
        self._cost_history = []

    def run_algorithm(self, backend: Backend, input: dict[str, Any]) -> VariationalResult:
        """Execute the variational optimization loop.

        Returns:
            VariationalResult with counts, optimal parameters, and cost history.
        """
        cl_cost_fn: Callable = input["cl_cost_function"]
        num_qubits: int = input["num_qubits"]

        # 1. Build the quantum cost function (closure over backend + ansatz)
        self._cost_history: list[float] = []
        cost_fn = self._make_cost_closure(backend, cl_cost_fn, num_qubits)

        # 2. Initialize parameters
        init_params = self._initialize_params()

        # 3. Run optimizer (it drives the variational loop via .minimize())
        opt_result = self.optimizer.minimize(cost_fn, init_params)
        optimal_params = self._extract_params(opt_result)

        # 4. Final measurement with optimized parameters
        final_counts = self._measure_final(backend, optimal_params, num_qubits)

        return VariationalResult(
            counts=final_counts,
            optimal_params=optimal_params,
            cost_history=list(self._cost_history),
            num_evals=len(self._cost_history),
        )

    def _make_cost_closure(
        self,
        backend: Backend,
        cl_cost_fn: Callable[[dict[str, int]], float],
        num_qubits: int,
    ) -> Callable[[np.ndarray], float]:
        """Build the cost function that the optimizer will call repeatedly.

        Each invocation: create QuantumVariable → apply ansatz → run on backend → evaluate cost.
        """

        def cost_fn(params: np.ndarray) -> float:
            from qrisp import QuantumVariable

            qv = QuantumVariable(num_qubits)
            self.ansatz(qv, params)
            counts = backend.run(qv, shots=self.shots)
            cost = cl_cost_fn(counts)
            self._cost_history.append(cost)
            return cost

        return cost_fn

    def _initialize_params(self) -> np.ndarray:
        """Create initial parameter array.

        Uses init_params if provided (warm-start), otherwise random in [0, 2π).
        """
        if self.init_params is not None:
            return np.asarray(self.init_params, dtype=float)
        num_params = self.ansatz.num_params
        return np.random.uniform(0, 2 * np.pi, size=num_params)

    @staticmethod
    def _extract_params(opt_result: Any) -> np.ndarray:
        """Extract optimal parameters from optimizer result.

        Handles results with .x attribute (qiskit_algorithms, scipy) or plain ndarray.
        """
        if hasattr(opt_result, "x"):
            return np.asarray(opt_result.x)
        return np.asarray(opt_result)

    def _measure_final(
        self, backend: Backend, params: np.ndarray, num_qubits: int
    ) -> dict[str, int]:
        """Run the final optimized circuit and return measurement counts."""
        from qrisp import QuantumVariable

        qv = QuantumVariable(num_qubits)
        self.ansatz(qv, params)
        return backend.run(qv, shots=self.shots)

HYPERPARAMS class-attribute

HYPERPARAMS = [
    HyperParam(
        name="ansatz",
        hparam_type=None,
        description="Parameterized ansatz: callable(qv, params) with .num_params attribute",
    ),
    HyperParam(
        name="optimizer",
        hparam_type=None,
        description="Optimizer instance with .minimize(fun, x0, bounds=None) → result with .x. Built by OptimizerBuilder.",
    ),
    HyperParam(
        name="shots",
        hparam_type=int,
        description="Number of shots per circuit evaluation",
        default=1024,
        test=lambda x: x > 0,
    ),
    HyperParam(
        name="init_params",
        hparam_type=None,
        description="Optional initial parameter array for warm-starting (None = random)",
        default=None,
    ),
]

INPUT_KEYS class-attribute

INPUT_KEYS = ('cl_cost_function', 'num_qubits')

reset

reset()

Clear input, backend, and algorithm state after execution.

Source code in src/quast_decisiontree/algorithms/hybrid/variational.py
 99
100
101
102
def reset(self) -> None:
    """Clear input, backend, and algorithm state after execution."""
    super().reset()
    self._cost_history = []

run_algorithm

run_algorithm(backend, input)

Execute the variational optimization loop.

Returns:

Type Description
VariationalResult

VariationalResult with counts, optimal parameters, and cost history.

Source code in src/quast_decisiontree/algorithms/hybrid/variational.py
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
def run_algorithm(self, backend: Backend, input: dict[str, Any]) -> VariationalResult:
    """Execute the variational optimization loop.

    Returns:
        VariationalResult with counts, optimal parameters, and cost history.
    """
    cl_cost_fn: Callable = input["cl_cost_function"]
    num_qubits: int = input["num_qubits"]

    # 1. Build the quantum cost function (closure over backend + ansatz)
    self._cost_history: list[float] = []
    cost_fn = self._make_cost_closure(backend, cl_cost_fn, num_qubits)

    # 2. Initialize parameters
    init_params = self._initialize_params()

    # 3. Run optimizer (it drives the variational loop via .minimize())
    opt_result = self.optimizer.minimize(cost_fn, init_params)
    optimal_params = self._extract_params(opt_result)

    # 4. Final measurement with optimized parameters
    final_counts = self._measure_final(backend, optimal_params, num_qubits)

    return VariationalResult(
        counts=final_counts,
        optimal_params=optimal_params,
        cost_history=list(self._cost_history),
        num_evals=len(self._cost_history),
    )