Skip to content

quast_decisiontree.algorithms.hybrid

quast_decisiontree.algorithms.hybrid

__all__ module-attribute

__all__ = [
    "HybridAlgorithm",
    "HybridAlgorithmBuilder",
    "QAOAResult",
    "QrispQAOA",
    "QrispVQE",
    "VQEResult",
    "VariationalAlgorithm",
    "VariationalResult",
]

HybridAlgorithmBuilder

Bases: AutoClassBuilder

Builder specialized for HybridAlgorithm subclasses.

Nodes can discover available hybrid algorithms by scanning for all instances of this class (e.g., in a registry or module-level list).

Source code in src/quast_decisiontree/algorithms/hybrid/builder.py
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
class HybridAlgorithmBuilder(AutoClassBuilder):
    """Builder specialized for HybridAlgorithm subclasses.

    Nodes can discover available hybrid algorithms by scanning for all
    instances of this class (e.g., in a registry or module-level list).
    """

    def __init__(
        self,
        superclass,
        hyperparams: list,
        name: str | None = None,
        description: str | None = None,
        input_keys: tuple = (),
    ) -> None:
        super().__init__(
            superclass=superclass,
            hyperparams=hyperparams,
            name=name,
            description=description,
        )
        self.input_keys = input_keys

    @property
    def supported_inputs(self) -> tuple:
        """The INPUT_KEYS of the algorithm this builder constructs."""
        return self.input_keys

input_keys instance-attribute

input_keys = input_keys

supported_inputs property

supported_inputs

The INPUT_KEYS of the algorithm this builder constructs.

__init__

__init__(
    superclass,
    hyperparams,
    name=None,
    description=None,
    input_keys=(),
)
Source code in src/quast_decisiontree/algorithms/hybrid/builder.py
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
def __init__(
    self,
    superclass,
    hyperparams: list,
    name: str | None = None,
    description: str | None = None,
    input_keys: tuple = (),
) -> None:
    super().__init__(
        superclass=superclass,
        hyperparams=hyperparams,
        name=name,
        description=description,
    )
    self.input_keys = input_keys

HybridAlgorithm

Bases: ABC

Base class for hybrid quantum-classical algorithms.

Integrates with AutoClassBuilder: subclasses declare HYPERPARAMS and their init receives kwargs matching those names.

Subclasses declare INPUT_KEYS to specify which problem data they need, then implement run_algorithm() with the actual logic.

Source code in src/quast_decisiontree/algorithms/hybrid/hybrid_algorithm.py
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 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
class HybridAlgorithm(ABC):
    """Base class for hybrid quantum-classical algorithms.

    Integrates with AutoClassBuilder: subclasses declare HYPERPARAMS
    and their __init__ receives kwargs matching those names.

    Subclasses declare INPUT_KEYS to specify which problem data they need,
    then implement run_algorithm() with the actual logic.
    """

    HYPERPARAMS: ClassVar[list] = []
    """List of HyperParam instances; override in subclass."""

    INPUT_KEYS: ClassVar[Sequence[str]] = ()
    """Keys the algorithm expects (e.g., 'qubo_matrix', 'graph', 'cost_operator')."""

    def __init__(self, **kwargs: Any) -> None:
        """Initialize from keyword arguments matching HYPERPARAMS names.

        This constructor signature is compatible with AutoClassBuilder.build().
        """
        for hp in self.HYPERPARAMS:
            value = kwargs.get(hp.name, hp.default if hp.has_default else None)
            setattr(self, hp.name, value)
        self._reset_input()
        self.backend: Backend | None = None

    # --- Input management ---

    @property
    def input(self) -> dict[str, Any]:
        return self._input

    def set_input(self, input_dict: dict[str, Any]) -> None:
        """Validate and store input data. Raises on unknown keys."""
        self._reset_input()
        # FIX: was `for key, val in input_dict:` — needs .items()
        for key, val in input_dict.items():
            if key in self.INPUT_KEYS:
                self._input[key] = val
            else:
                raise ValueError(
                    f"Invalid input key {key!r} for {self.__class__.__name__}. "
                    f"Allowed: {list(self.INPUT_KEYS)}"
                )

    def _reset_input(self) -> None:
        self._input = {key: None for key in self.INPUT_KEYS}

    def _validate_input(self) -> None:
        """Check that all required keys have been set (are not None)."""
        missing = [k for k, v in self._input.items() if v is None]
        if missing:
            raise ValueError(f"{self.__class__.__name__} missing required inputs: {missing}")

    def reset(self) -> None:
        """Clear input and backend state after execution."""
        self._reset_input()
        self.backend = None

    # --- Execution ---

    def execute(self, backend: Backend, **kwargs: Any) -> Any:
        """Execute the hybrid algorithm.

        Args:
            backend: Backend instance supporting run()/run_batch().
            **kwargs: Must match INPUT_KEYS (e.g., qubo_matrix=..., graph=...).

        Returns:
            Algorithm-specific result.
        """
        self.set_input(kwargs)
        self._validate_input()
        self.backend = backend
        try:
            result = self.run_algorithm(backend=self.backend, input=self._input)
        finally:
            # Always clean up, even on error
            self.reset()
        return result

    @abstractmethod
    def run_algorithm(self, backend: Backend, input: dict[str, Any]) -> Any:
        """Algorithm-specific execution. Override in subclass.

        Backend usage:
            - backend(circuit, shots=None) or backend.run(circuit, shots=None)
            - backend.run_batch(circuits, shots=None)

        Backends return Dict[str, int] mapping bitstrings to counts.

        Args:
            backend: The quantum backend to submit circuits to.
            input: Dict with keys from INPUT_KEYS, all guaranteed non-None.

        Returns:
            Algorithm result (solution dict, counts, optimal value, etc.)
        """

    # --- Builder integration ---

    @classmethod
    def get_builder(cls, name: str | None = None, description: str | None = None):
        """Create an AutoClassBuilder for this algorithm class."""
        from quast_decisiontree.algorithms.hybrid.builder import HybridAlgorithmBuilder
        from quast_decisiontree.core.builder import HyperParam

        # Copy hyperparams so builders don't share mutable state
        copied = [
            HyperParam(
                name=hp.name,
                hparam_type=hp.type,
                description=hp.description,
                default=hp.default if hp.has_default else "",
                test=hp.test,
                allow_multiple=hp.allow_multiple,
            )
            for hp in cls.HYPERPARAMS
        ]
        return HybridAlgorithmBuilder(
            superclass=cls,
            hyperparams=copied,
            name=name or cls.__name__,
            description=description or cls.__doc__ or "",
            input_keys=cls.INPUT_KEYS,
        )

HYPERPARAMS class-attribute

HYPERPARAMS = []

List of HyperParam instances; override in subclass.

INPUT_KEYS class-attribute

INPUT_KEYS = ()

Keys the algorithm expects (e.g., 'qubo_matrix', 'graph', 'cost_operator').

backend instance-attribute

backend = None

input property

input

__init__

__init__(**kwargs)

Initialize from keyword arguments matching HYPERPARAMS names.

This constructor signature is compatible with AutoClassBuilder.build().

Source code in src/quast_decisiontree/algorithms/hybrid/hybrid_algorithm.py
32
33
34
35
36
37
38
39
40
41
def __init__(self, **kwargs: Any) -> None:
    """Initialize from keyword arguments matching HYPERPARAMS names.

    This constructor signature is compatible with AutoClassBuilder.build().
    """
    for hp in self.HYPERPARAMS:
        value = kwargs.get(hp.name, hp.default if hp.has_default else None)
        setattr(self, hp.name, value)
    self._reset_input()
    self.backend: Backend | None = None

set_input

set_input(input_dict)

Validate and store input data. Raises on unknown keys.

Source code in src/quast_decisiontree/algorithms/hybrid/hybrid_algorithm.py
49
50
51
52
53
54
55
56
57
58
59
60
def set_input(self, input_dict: dict[str, Any]) -> None:
    """Validate and store input data. Raises on unknown keys."""
    self._reset_input()
    # FIX: was `for key, val in input_dict:` — needs .items()
    for key, val in input_dict.items():
        if key in self.INPUT_KEYS:
            self._input[key] = val
        else:
            raise ValueError(
                f"Invalid input key {key!r} for {self.__class__.__name__}. "
                f"Allowed: {list(self.INPUT_KEYS)}"
            )

reset

reset()

Clear input and backend state after execution.

Source code in src/quast_decisiontree/algorithms/hybrid/hybrid_algorithm.py
71
72
73
74
def reset(self) -> None:
    """Clear input and backend state after execution."""
    self._reset_input()
    self.backend = None

execute

execute(backend, **kwargs)

Execute the hybrid algorithm.

Parameters:

Name Type Description Default
backend Backend

Backend instance supporting run()/run_batch().

required
**kwargs Any

Must match INPUT_KEYS (e.g., qubo_matrix=..., graph=...).

{}

Returns:

Type Description
Any

Algorithm-specific result.

Source code in src/quast_decisiontree/algorithms/hybrid/hybrid_algorithm.py
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
def execute(self, backend: Backend, **kwargs: Any) -> Any:
    """Execute the hybrid algorithm.

    Args:
        backend: Backend instance supporting run()/run_batch().
        **kwargs: Must match INPUT_KEYS (e.g., qubo_matrix=..., graph=...).

    Returns:
        Algorithm-specific result.
    """
    self.set_input(kwargs)
    self._validate_input()
    self.backend = backend
    try:
        result = self.run_algorithm(backend=self.backend, input=self._input)
    finally:
        # Always clean up, even on error
        self.reset()
    return result

run_algorithm abstractmethod

run_algorithm(backend, input)

Algorithm-specific execution. Override in subclass.

Backend usage
  • backend(circuit, shots=None) or backend.run(circuit, shots=None)
  • backend.run_batch(circuits, shots=None)

Backends return Dict[str, int] mapping bitstrings to counts.

Parameters:

Name Type Description Default
backend Backend

The quantum backend to submit circuits to.

required
input dict[str, Any]

Dict with keys from INPUT_KEYS, all guaranteed non-None.

required

Returns:

Type Description
Any

Algorithm result (solution dict, counts, optimal value, etc.)

Source code in src/quast_decisiontree/algorithms/hybrid/hybrid_algorithm.py
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
@abstractmethod
def run_algorithm(self, backend: Backend, input: dict[str, Any]) -> Any:
    """Algorithm-specific execution. Override in subclass.

    Backend usage:
        - backend(circuit, shots=None) or backend.run(circuit, shots=None)
        - backend.run_batch(circuits, shots=None)

    Backends return Dict[str, int] mapping bitstrings to counts.

    Args:
        backend: The quantum backend to submit circuits to.
        input: Dict with keys from INPUT_KEYS, all guaranteed non-None.

    Returns:
        Algorithm result (solution dict, counts, optimal value, etc.)
    """

get_builder classmethod

get_builder(name=None, description=None)

Create an AutoClassBuilder for this algorithm class.

Source code in src/quast_decisiontree/algorithms/hybrid/hybrid_algorithm.py
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
@classmethod
def get_builder(cls, name: str | None = None, description: str | None = None):
    """Create an AutoClassBuilder for this algorithm class."""
    from quast_decisiontree.algorithms.hybrid.builder import HybridAlgorithmBuilder
    from quast_decisiontree.core.builder import HyperParam

    # Copy hyperparams so builders don't share mutable state
    copied = [
        HyperParam(
            name=hp.name,
            hparam_type=hp.type,
            description=hp.description,
            default=hp.default if hp.has_default else "",
            test=hp.test,
            allow_multiple=hp.allow_multiple,
        )
        for hp in cls.HYPERPARAMS
    ]
    return HybridAlgorithmBuilder(
        superclass=cls,
        hyperparams=copied,
        name=name or cls.__name__,
        description=description or cls.__doc__ or "",
        input_keys=cls.INPUT_KEYS,
    )

QAOAResult dataclass

Bases: VariationalResult

Result container for QrispQAOA execution.

Inherits from VariationalResult

counts, optimal_params, cost_history, num_evals

Additional attributes

best_bitstring: The bitstring with the lowest cost. best_cost: The cost value of the best bitstring.

Source code in src/quast_decisiontree/algorithms/hybrid/qrisp_qaoa.py
27
28
29
30
31
32
33
34
35
36
37
38
39
40
@dataclass
class QAOAResult(VariationalResult):
    """Result container for QrispQAOA execution.

    Inherits from VariationalResult:
        counts, optimal_params, cost_history, num_evals

    Additional attributes:
        best_bitstring: The bitstring with the lowest cost.
        best_cost: The cost value of the best bitstring.
    """

    best_bitstring: str = ""
    best_cost: float = float("inf")

best_bitstring class-attribute instance-attribute

best_bitstring = ''

best_cost class-attribute instance-attribute

best_cost = float('inf')

__init__

__init__(
    counts,
    optimal_params=None,
    cost_history=list(),
    num_evals=0,
    best_bitstring="",
    best_cost=float("inf"),
)

QrispQAOA

Bases: HybridAlgorithm

QAOA implementation delegating to Qrisp's QAOAProblem.

Keeps all modifiable parameters (reps, optimizer, max_iter, etc.) as hyperparameters configurable via the builder system. Problem-specific inputs (cost_operator, mixer, cl_cost_function, num_qubits) are provided at execution time.

Compatible with the decision tree node flow

SelectLayersNode → sets reps QrispMixerNode → sets mixer (Qrisp-compatible callable) SelectOptimizerNode → sets optimizer QrispQAOASetupNode → instantiates this class HybridAlgorithmExecuteNode → calls execute()

Example usage (standalone)::

qaoa = QrispQAOA(reps=3, max_iter=100, optimizer="COBYLA")
result = qaoa.execute(
    backend=my_backend,
    cost_operator=my_cost_op,
    mixer=my_mixer,
    cl_cost_function=my_cl_cost,
    num_qubits=5,
)
print(result.best_bitstring, result.best_cost)
Source code in src/quast_decisiontree/algorithms/hybrid/qrisp_qaoa.py
 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
186
187
188
189
190
191
192
193
194
195
class QrispQAOA(HybridAlgorithm):
    """QAOA implementation delegating to Qrisp's QAOAProblem.

    Keeps all modifiable parameters (reps, optimizer, max_iter, etc.) as
    hyperparameters configurable via the builder system. Problem-specific
    inputs (cost_operator, mixer, cl_cost_function, num_qubits) are provided
    at execution time.

    Compatible with the decision tree node flow:
        SelectLayersNode → sets reps
        QrispMixerNode → sets mixer (Qrisp-compatible callable)
        SelectOptimizerNode → sets optimizer
        QrispQAOASetupNode → instantiates this class
        HybridAlgorithmExecuteNode → calls execute()

    Example usage (standalone)::

        qaoa = QrispQAOA(reps=3, max_iter=100, optimizer="COBYLA")
        result = qaoa.execute(
            backend=my_backend,
            cost_operator=my_cost_op,
            mixer=my_mixer,
            cl_cost_function=my_cl_cost,
            num_qubits=5,
        )
        print(result.best_bitstring, result.best_cost)
    """

    HYPERPARAMS: ClassVar[list] = [
        HyperParam(
            name="reps",
            hparam_type=int,
            description="Number of QAOA layers (depth parameter p)",
            default=1,
            test=lambda x: x > 0,
        ),
        HyperParam(
            name="max_iter",
            hparam_type=int,
            description="Maximum number of classical optimizer iterations",
            default=50,
            test=lambda x: x > 0,
        ),
        HyperParam(
            name="optimizer",
            hparam_type=None,
            description=(
                "Classical optimizer: a string name for scipy.optimize.minimize "
                "(e.g. 'COBYLA', 'Nelder-Mead') or an object with a "
                ".minimize(fun, x0) method (e.g. from OptimizerBuilder)"
            ),
            default="COBYLA",
        ),
        HyperParam(
            name="init_type",
            hparam_type=str,
            description="Parameter initialization strategy ('random' or 'tqa')",
            default="random",
        ),
        HyperParam(
            name="init_params",
            hparam_type=None,
            description="Initial variational parameters (array-like, optional)",
            default=None,
        ),
    ]

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

    def run_algorithm(self, backend: Backend, input: dict[str, Any]) -> QAOAResult:
        """Execute QAOA via Qrisp's QAOAProblem.

        Args:
            backend: The backend to run quantum circuits on.
            input: Dict with keys from INPUT_KEYS.

        Returns:
            QAOAResult with counts, best solution, and metadata.
        """
        problem = QAOAProblem(
            cost_operator=input["cost_operator"],
            mixer=input["mixer"],
            cl_cost_function=input["cl_cost_function"],
        )

        qarg = QuantumVariable(input["num_qubits"])

        run_kwargs = self._build_run_kwargs(backend)

        result_counts = problem.run(qarg, **run_kwargs)

        best_bitstring, best_cost = self._evaluate_best(result_counts, input["cl_cost_function"])

        return QAOAResult(
            counts=result_counts,
            best_bitstring=best_bitstring,
            best_cost=best_cost,
        )

    def _build_run_kwargs(self, backend: Backend) -> dict[str, Any]:
        """Assemble keyword arguments for QAOAProblem.run()."""
        run_kwargs: dict[str, Any] = {
            "depth": self.reps,
            "max_iter": self.max_iter,
            "init_type": self.init_type,
        }

        if self.init_params is not None:
            run_kwargs["init_point"] = np.asarray(self.init_params, dtype=float)

        adapted_optimizer = adapt_optimizer(self.optimizer)
        if adapted_optimizer is not None:
            run_kwargs["optimizer"] = adapted_optimizer

        # Only pass backend if it has a working submit function
        # Otherwise let Qrisp use its built-in simulator
        if backend is not None and backend.has_submit:
            run_kwargs["mes_kwargs"] = {"backend": backend}
        else:
            logger = logging.getLogger("dt_logger")
            logger.warning(
                "Backend '%s' has no submit function and cannot be used by Qrisp. "
                "Falling back to Qrisp's built-in simulator.",
                backend.get("name", repr(backend)),
            )
        return run_kwargs

    @staticmethod
    def _evaluate_best(counts: dict[str, float], cl_cost_function: Any) -> tuple:
        """Find the bitstring with the lowest cost from the result distribution.

        Args:
            counts: {bitstring: probability} mapping from Qrisp.
            cl_cost_function: Classical cost function mapping counts to cost.

        Returns:
            Tuple of (best_bitstring, best_cost).
        """
        best_bitstring = ""
        best_cost = float("inf")

        for bitstring in counts:
            cost = cl_cost_function({bitstring: 1})
            if cost < best_cost:
                best_cost = cost
                best_bitstring = bitstring

        return best_bitstring, best_cost

HYPERPARAMS class-attribute

HYPERPARAMS = [
    HyperParam(
        name="reps",
        hparam_type=int,
        description="Number of QAOA layers (depth parameter p)",
        default=1,
        test=lambda x: x > 0,
    ),
    HyperParam(
        name="max_iter",
        hparam_type=int,
        description="Maximum number of classical optimizer iterations",
        default=50,
        test=lambda x: x > 0,
    ),
    HyperParam(
        name="optimizer",
        hparam_type=None,
        description="Classical optimizer: a string name for scipy.optimize.minimize (e.g. 'COBYLA', 'Nelder-Mead') or an object with a .minimize(fun, x0) method (e.g. from OptimizerBuilder)",
        default="COBYLA",
    ),
    HyperParam(
        name="init_type",
        hparam_type=str,
        description="Parameter initialization strategy ('random' or 'tqa')",
        default="random",
    ),
    HyperParam(
        name="init_params",
        hparam_type=None,
        description="Initial variational parameters (array-like, optional)",
        default=None,
    ),
]

INPUT_KEYS class-attribute

INPUT_KEYS = (
    "cost_operator",
    "mixer",
    "cl_cost_function",
    "num_qubits",
)

run_algorithm

run_algorithm(backend, input)

Execute QAOA via Qrisp's QAOAProblem.

Parameters:

Name Type Description Default
backend Backend

The backend to run quantum circuits on.

required
input dict[str, Any]

Dict with keys from INPUT_KEYS.

required

Returns:

Type Description
QAOAResult

QAOAResult with counts, best solution, and metadata.

Source code in src/quast_decisiontree/algorithms/hybrid/qrisp_qaoa.py
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
def run_algorithm(self, backend: Backend, input: dict[str, Any]) -> QAOAResult:
    """Execute QAOA via Qrisp's QAOAProblem.

    Args:
        backend: The backend to run quantum circuits on.
        input: Dict with keys from INPUT_KEYS.

    Returns:
        QAOAResult with counts, best solution, and metadata.
    """
    problem = QAOAProblem(
        cost_operator=input["cost_operator"],
        mixer=input["mixer"],
        cl_cost_function=input["cl_cost_function"],
    )

    qarg = QuantumVariable(input["num_qubits"])

    run_kwargs = self._build_run_kwargs(backend)

    result_counts = problem.run(qarg, **run_kwargs)

    best_bitstring, best_cost = self._evaluate_best(result_counts, input["cl_cost_function"])

    return QAOAResult(
        counts=result_counts,
        best_bitstring=best_bitstring,
        best_cost=best_cost,
    )

QrispVQE

Bases: HybridAlgorithm

Measurement-based VQE implementation using Qrisp circuits.

Uses a sampling approach: prepare parameterized state → measure → compute energy from counts by iterating over Hamiltonian terms. This avoids Qrisp's VQEProblem.expectation_value() which has memory issues with large Hamiltonians due to JAX graph accumulation.

Energy evaluation uses QubitOperator.terms_dict to iterate over Hamiltonian terms and QubitTerm.factor_dict for per-qubit Pauli type lookup. For diagonal (Z-only) Hamiltonians, this yields exact per-bitstring energies. Non-Z terms contribute zero in the computational basis and are handled gracefully.

Compatible with the decision tree node flow

QrispAnsatzNode → sets ansatz_function + num_params SelectOptimizerNode → sets optimizer SelectBackendNode → sets backend QrispVQESetupNode → instantiates this class HybridAlgorithmExecuteNode → calls execute()

Example usage (standalone)::

vqe = QrispVQE(depth=1, max_iter=100, optimizer="COBYLA")
result = vqe.execute(
    backend=my_backend,
    hamiltonian=my_hamiltonian,
    ansatz_function=my_ansatz,
    num_params=8,
    num_qubits=4,
)
print(result.energy, result.best_bitstring)
Source code in src/quast_decisiontree/algorithms/hybrid/qrisp_vqe.py
 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
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
class QrispVQE(HybridAlgorithm):
    """Measurement-based VQE implementation using Qrisp circuits.

    Uses a sampling approach: prepare parameterized state → measure → compute
    energy from counts by iterating over Hamiltonian terms. This avoids Qrisp's
    VQEProblem.expectation_value() which has memory issues with large
    Hamiltonians due to JAX graph accumulation.

    Energy evaluation uses QubitOperator.terms_dict to iterate over Hamiltonian
    terms and QubitTerm.factor_dict for per-qubit Pauli type lookup. For
    diagonal (Z-only) Hamiltonians, this yields exact per-bitstring energies.
    Non-Z terms contribute zero in the computational basis and are handled
    gracefully.

    Compatible with the decision tree node flow:
        QrispAnsatzNode → sets ansatz_function + num_params
        SelectOptimizerNode → sets optimizer
        SelectBackendNode → sets backend
        QrispVQESetupNode → instantiates this class
        HybridAlgorithmExecuteNode → calls execute()

    Example usage (standalone)::

        vqe = QrispVQE(depth=1, max_iter=100, optimizer="COBYLA")
        result = vqe.execute(
            backend=my_backend,
            hamiltonian=my_hamiltonian,
            ansatz_function=my_ansatz,
            num_params=8,
            num_qubits=4,
        )
        print(result.energy, result.best_bitstring)
    """

    HYPERPARAMS: ClassVar[list] = [
        HyperParam(
            name="depth",
            hparam_type=int,
            description="Number of ansatz repetitions (layers)",
            default=1,
            test=lambda x: x > 0,
        ),
        HyperParam(
            name="max_iter",
            hparam_type=int,
            description="Maximum number of classical optimizer iterations",
            default=50,
            test=lambda x: x > 0,
        ),
        HyperParam(
            name="optimizer",
            hparam_type=None,
            description=(
                "Classical optimizer: a string name for scipy.optimize.minimize "
                "(e.g. 'COBYLA', 'Nelder-Mead') or an object with a "
                ".minimize(fun, x0) method (e.g. from OptimizerBuilder)"
            ),
            default="COBYLA",
        ),
        HyperParam(
            name="shots",
            hparam_type=int,
            description="Number of measurement shots per evaluation",
            default=1024,
            test=lambda x: x > 0,
        ),
        HyperParam(
            name="init_params",
            hparam_type=None,
            description="Initial variational parameters (array-like, optional)",
            default=None,
        ),
    ]

    INPUT_KEYS: ClassVar[Sequence[str]] = (
        "hamiltonian",
        "ansatz_function",
        "num_params",
        "num_qubits",
    )

    def run_algorithm(self, backend: Backend, input: dict[str, Any]) -> VQEResult:
        """Execute measurement-based VQE.

        Args:
            backend: The backend to run quantum circuits on.
            input: Dict with keys from INPUT_KEYS.

        Returns:
            VQEResult with energy, counts, best solution, and metadata.
        """
        hamiltonian = input["hamiltonian"]
        ansatz_function = input["ansatz_function"]
        num_params = input["num_params"]
        num_qubits = input["num_qubits"]
        total_params = num_params * self.depth

        logger.debug(
            "QrispVQE.run_algorithm called with num_qubits=%d, num_params=%d, "
            "depth=%d, total_params=%d",
            num_qubits,
            num_params,
            self.depth,
            total_params,
        )
        logger.debug(
            "QrispVQE hyperparameters: max_iter=%d, optimizer=%r, shots=%d",
            self.max_iter,
            self.optimizer,
            self.shots,
        )

        terms_dict = hamiltonian.terms_dict
        logger.debug("Hamiltonian has %d terms.", len(terms_dict))

        self._cost_history: list[float] = []

        cost_fn = self._make_cost_closure(
            backend, terms_dict, ansatz_function, num_params, num_qubits
        )

        init_params = self._initialize_params(total_params)
        logger.debug("Initial parameters shape: %s", init_params.shape)

        logger.debug("Starting optimization loop.")
        opt_result = self._run_optimizer(cost_fn, init_params)
        optimal_params = self._extract_params(opt_result)
        logger.debug(
            "Optimization complete after %d evaluations. Final energy: %s",
            len(self._cost_history),
            self._cost_history[-1] if self._cost_history else "N/A",
        )

        # Final measurement with optimal parameters
        logger.debug("Performing final measurement with optimal parameters.")
        final_counts = self._measure(
            backend, optimal_params, ansatz_function, num_params, num_qubits
        )
        logger.debug(
            "Final measurement: %d unique bitstrings from %d shots.",
            len(final_counts),
            sum(final_counts.values()),
        )

        # Find best bitstring
        best_bitstring, best_energy = self._evaluate_best(final_counts, terms_dict)
        logger.debug("Best bitstring: %r with energy: %.6f", best_bitstring, best_energy)

        final_energy = self._cost_history[-1] if self._cost_history else best_energy

        return VQEResult(
            counts=final_counts,
            energy=final_energy,
            best_bitstring=best_bitstring,
            optimal_params=optimal_params,
            cost_history=list(self._cost_history),
            num_evals=len(self._cost_history),
        )

    def _make_cost_closure(self, backend, terms_dict, ansatz_function, num_params, num_qubits):
        """Create the cost function closure for the optimizer."""

        def cost_fn(params):
            counts = self._measure(backend, params, ansatz_function, num_params, num_qubits)
            energy = self._compute_energy(counts, terms_dict)
            self._cost_history.append(energy)

            if len(self._cost_history) % 10 == 0:
                logger.debug(
                    "VQE iteration %d: energy = %.6f",
                    len(self._cost_history),
                    energy,
                )

            return energy

        return cost_fn

    def _measure(self, backend, params, ansatz_function, num_params, num_qubits) -> dict[str, int]:
        """Prepare parameterized state and measure.

        Args:
            backend: The backend to run quantum circuits on.
            params: Full parameter vector (length = num_params * depth).
            ansatz_function: Callable(qv, layer_params) applying one ansatz layer.
            num_params: Number of parameters per ansatz layer.
            num_qubits: Number of qubits.

        Returns:
            Measurement counts as {bitstring: count}.
        """
        from qrisp import QuantumVariable

        qv = QuantumVariable(num_qubits)

        # Apply ansatz layers
        for d in range(self.depth):
            layer_params = params[d * num_params : (d + 1) * num_params]
            ansatz_function(qv, layer_params)

        # Measure
        mes_kwargs = {}
        if backend is not None and backend.has_submit:
            backend_name = backend.get("name", "")
            if backend_name != "qrisp_simulator":
                mes_kwargs["backend"] = backend

        return qv.get_measurement(shots=self.shots, **mes_kwargs)

    @staticmethod
    def _bitstring_energy(bitstring: str, terms_dict: dict) -> float:
        """Compute energy of a single bitstring for a Hamiltonian.

        For each term: coeff * Π_{i in qubits} eigenvalue(pauli_i, bit_i)
        Z eigenvalues: |0⟩ → +1, |1⟩ → -1.
        Non-Z (X, Y) terms contribute zero in the computational basis.

        Args:
            bitstring: Measured bitstring (qubit 0 is leftmost).
            terms_dict: QubitOperator.terms_dict mapping QubitTerm → coefficient.

        Returns:
            Energy of the bitstring.
        """
        energy = 0.0
        for term, coeff in terms_dict.items():
            term_val = float(coeff)
            for qubit_idx, pauli in term.factor_dict.items():
                if pauli == "Z":
                    bit = int(bitstring[qubit_idx])
                    term_val *= 1 - 2 * bit
                else:
                    # X and Y have zero expectation value in computational basis
                    term_val = 0.0
                    break
            energy += term_val
        return energy

    @staticmethod
    def _compute_energy(counts: dict[str, float], terms_dict: dict) -> float:
        """Compute expectation value from measurement counts.

        Evaluates E = Σ_b (count_b / total) * E(b) where E(b) is the
        energy of bitstring b computed via term iteration.

        Args:
            counts: {bitstring: count} mapping from measurement.
            terms_dict: QubitOperator.terms_dict for term iteration.

        Returns:
            Estimated expectation value of the Hamiltonian.
        """
        total_shots = sum(counts.values())
        if total_shots == 0:
            return float("inf")

        energy = 0.0
        for bitstring, count in counts.items():
            energy += (count / total_shots) * QrispVQE._bitstring_energy(bitstring, terms_dict)
        return energy

    @staticmethod
    def _evaluate_best(counts: dict[str, float], terms_dict: dict) -> tuple:
        """Find the measured bitstring with the lowest energy.

        Args:
            counts: {bitstring: count} mapping from measurement.
            terms_dict: QubitOperator.terms_dict for term iteration.

        Returns:
            Tuple of (best_bitstring, best_energy).
        """
        if not counts:
            return "", float("inf")

        best_bitstring = ""
        best_energy = float("inf")

        for bitstring in counts:
            bitstring_energy = QrispVQE._bitstring_energy(bitstring, terms_dict)
            if bitstring_energy < best_energy:
                best_energy = bitstring_energy
                best_bitstring = bitstring

        return best_bitstring, best_energy

    def _initialize_params(self, total_params: int) -> np.ndarray:
        """Initialize variational parameters.

        Uses init_params if provided and length matches, otherwise
        random uniform in [0, 2π).
        """
        if self.init_params is not None:
            params = np.asarray(self.init_params, dtype=float)
            if len(params) != total_params:
                logger.warning(
                    "init_params length (%d) != total_params (%d). Using random initialization.",
                    len(params),
                    total_params,
                )
                return np.random.uniform(0, 2 * np.pi, size=total_params)
            return params
        return np.random.uniform(0, 2 * np.pi, size=total_params)

    def _run_optimizer(self, cost_fn, init_params: np.ndarray):
        """Run the classical optimization loop.

        Delegates to scipy.optimize.minimize or a custom optimizer
        adapted via adapt_optimizer().
        """
        adapted = adapt_optimizer(self.optimizer)

        if adapted is None or isinstance(adapted, str):
            method = adapted if isinstance(adapted, str) else "COBYLA"
            logger.debug("Using scipy.optimize.minimize with method='%s'.", method)
            return scipy_minimize(
                cost_fn,
                init_params,
                method=method,
                options={"maxiter": self.max_iter},
            )
        elif callable(adapted):
            logger.debug("Using adapted custom optimizer.")
            return adapted(cost_fn, init_params)
        else:
            logger.debug("Using COBYLA as fallback optimizer.")
            return scipy_minimize(
                cost_fn,
                init_params,
                method="COBYLA",
                options={"maxiter": self.max_iter},
            )

    @staticmethod
    def _extract_params(opt_result) -> np.ndarray:
        """Extract parameter array from optimizer result."""
        if hasattr(opt_result, "x"):
            return np.asarray(opt_result.x)
        return np.asarray(opt_result)

HYPERPARAMS class-attribute

HYPERPARAMS = [
    HyperParam(
        name="depth",
        hparam_type=int,
        description="Number of ansatz repetitions (layers)",
        default=1,
        test=lambda x: x > 0,
    ),
    HyperParam(
        name="max_iter",
        hparam_type=int,
        description="Maximum number of classical optimizer iterations",
        default=50,
        test=lambda x: x > 0,
    ),
    HyperParam(
        name="optimizer",
        hparam_type=None,
        description="Classical optimizer: a string name for scipy.optimize.minimize (e.g. 'COBYLA', 'Nelder-Mead') or an object with a .minimize(fun, x0) method (e.g. from OptimizerBuilder)",
        default="COBYLA",
    ),
    HyperParam(
        name="shots",
        hparam_type=int,
        description="Number of measurement shots per evaluation",
        default=1024,
        test=lambda x: x > 0,
    ),
    HyperParam(
        name="init_params",
        hparam_type=None,
        description="Initial variational parameters (array-like, optional)",
        default=None,
    ),
]

INPUT_KEYS class-attribute

INPUT_KEYS = (
    "hamiltonian",
    "ansatz_function",
    "num_params",
    "num_qubits",
)

run_algorithm

run_algorithm(backend, input)

Execute measurement-based VQE.

Parameters:

Name Type Description Default
backend Backend

The backend to run quantum circuits on.

required
input dict[str, Any]

Dict with keys from INPUT_KEYS.

required

Returns:

Type Description
VQEResult

VQEResult with energy, counts, best solution, and metadata.

Source code in src/quast_decisiontree/algorithms/hybrid/qrisp_vqe.py
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
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
def run_algorithm(self, backend: Backend, input: dict[str, Any]) -> VQEResult:
    """Execute measurement-based VQE.

    Args:
        backend: The backend to run quantum circuits on.
        input: Dict with keys from INPUT_KEYS.

    Returns:
        VQEResult with energy, counts, best solution, and metadata.
    """
    hamiltonian = input["hamiltonian"]
    ansatz_function = input["ansatz_function"]
    num_params = input["num_params"]
    num_qubits = input["num_qubits"]
    total_params = num_params * self.depth

    logger.debug(
        "QrispVQE.run_algorithm called with num_qubits=%d, num_params=%d, "
        "depth=%d, total_params=%d",
        num_qubits,
        num_params,
        self.depth,
        total_params,
    )
    logger.debug(
        "QrispVQE hyperparameters: max_iter=%d, optimizer=%r, shots=%d",
        self.max_iter,
        self.optimizer,
        self.shots,
    )

    terms_dict = hamiltonian.terms_dict
    logger.debug("Hamiltonian has %d terms.", len(terms_dict))

    self._cost_history: list[float] = []

    cost_fn = self._make_cost_closure(
        backend, terms_dict, ansatz_function, num_params, num_qubits
    )

    init_params = self._initialize_params(total_params)
    logger.debug("Initial parameters shape: %s", init_params.shape)

    logger.debug("Starting optimization loop.")
    opt_result = self._run_optimizer(cost_fn, init_params)
    optimal_params = self._extract_params(opt_result)
    logger.debug(
        "Optimization complete after %d evaluations. Final energy: %s",
        len(self._cost_history),
        self._cost_history[-1] if self._cost_history else "N/A",
    )

    # Final measurement with optimal parameters
    logger.debug("Performing final measurement with optimal parameters.")
    final_counts = self._measure(
        backend, optimal_params, ansatz_function, num_params, num_qubits
    )
    logger.debug(
        "Final measurement: %d unique bitstrings from %d shots.",
        len(final_counts),
        sum(final_counts.values()),
    )

    # Find best bitstring
    best_bitstring, best_energy = self._evaluate_best(final_counts, terms_dict)
    logger.debug("Best bitstring: %r with energy: %.6f", best_bitstring, best_energy)

    final_energy = self._cost_history[-1] if self._cost_history else best_energy

    return VQEResult(
        counts=final_counts,
        energy=final_energy,
        best_bitstring=best_bitstring,
        optimal_params=optimal_params,
        cost_history=list(self._cost_history),
        num_evals=len(self._cost_history),
    )

VQEResult dataclass

Bases: VariationalResult

Result container for QrispVQE execution.

Inherits from VariationalResult

counts, optimal_params, cost_history, num_evals

Additional attributes

energy: The minimized energy eigenvalue. best_bitstring: The bitstring with the lowest energy (from final measurement).

Source code in src/quast_decisiontree/algorithms/hybrid/qrisp_vqe.py
28
29
30
31
32
33
34
35
36
37
38
39
40
41
@dataclass
class VQEResult(VariationalResult):
    """Result container for QrispVQE execution.

    Inherits from VariationalResult:
        counts, optimal_params, cost_history, num_evals

    Additional attributes:
        energy: The minimized energy eigenvalue.
        best_bitstring: The bitstring with the lowest energy (from final measurement).
    """

    energy: float = float("inf")
    best_bitstring: str = ""

energy class-attribute instance-attribute

energy = float('inf')

best_bitstring class-attribute instance-attribute

best_bitstring = ''

__init__

__init__(
    counts,
    optimal_params=None,
    cost_history=list(),
    num_evals=0,
    energy=float("inf"),
    best_bitstring="",
)

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),
    )

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,
)