• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

qiskit-community / qiskit-optimization / 14535859838

18 Apr 2025 01:25PM UTC coverage: 92.104% (+0.05%) from 92.056%
14535859838

push

github

web-flow
Fix copyright (#658)

5681 of 6168 relevant lines covered (92.1%)

0.92 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

90.64
/qiskit_optimization/algorithms/optimization_algorithm.py
1
# This code is part of a Qiskit project.
2
#
3
# (C) Copyright IBM 2020, 2025.
4
#
5
# This code is licensed under the Apache License, Version 2.0. You may
6
# obtain a copy of this license in the LICENSE.txt file in the root directory
7
# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0.
8
#
9
# Any modifications or derivative works of this code must retain this
10
# copyright notice, and modified files need to carry a notice indicating
11
# that they have been altered from the originals.
12

13
"""An abstract class for optimization algorithms in Qiskit optimization module."""
14
from __future__ import annotations
1✔
15

16
from abc import ABC, abstractmethod
1✔
17
from dataclasses import dataclass
1✔
18
from enum import Enum
1✔
19
from logging import getLogger
1✔
20
from typing import Any, Dict, List, Tuple, Type, Union, cast
1✔
21

22
import numpy as np
1✔
23
from qiskit.quantum_info import Statevector
1✔
24
from qiskit.result import QuasiDistribution
1✔
25

26
from ..converters.quadratic_program_to_qubo import QuadraticProgramConverter, QuadraticProgramToQubo
1✔
27
from ..exceptions import QiskitOptimizationError
1✔
28
from ..problems.quadratic_program import QuadraticProgram, Variable
1✔
29

30
logger = getLogger(__name__)
1✔
31

32

33
class OptimizationResultStatus(Enum):
1✔
34
    """Termination status of an optimization algorithm."""
35

36
    SUCCESS = 0
1✔
37
    """the optimization algorithm succeeded to find a feasible solution."""
38

39
    FAILURE = 1
1✔
40
    """the optimization algorithm ended in a failure."""
41

42
    INFEASIBLE = 2
1✔
43
    """the optimization algorithm obtained an infeasible solution."""
1✔
44

45

46
@dataclass
1✔
47
class SolutionSample:
1✔
48
    """A sample of an optimization solution."""
49

50
    x: np.ndarray
1✔
51
    """The values of the variables"""
52
    fval: float
1✔
53
    """The objective function value"""
54
    probability: float
1✔
55
    """The probability of this sample"""
56
    status: OptimizationResultStatus
1✔
57
    """The status of this sample"""
1✔
58

59

60
class OptimizationResult:
1✔
61
    """A base class for optimization results.
62

63
    The optimization algorithms return an object of the type ``OptimizationResult``
64
    with the information about the solution obtained.
65

66
    ``OptimizationResult`` allows users to get the value of a variable by specifying an index or
67
    a name as follows.
68

69
    Examples:
70
        >>> from qiskit_optimization import QuadraticProgram
71
        >>> from qiskit_optimization.algorithms import CplexOptimizer
72
        >>> problem = QuadraticProgram()
73
        >>> _ = problem.binary_var('x1')
74
        >>> _ = problem.binary_var('x2')
75
        >>> _ = problem.binary_var('x3')
76
        >>> problem.minimize(linear={'x1': 1, 'x2': -2, 'x3': 3})
77
        >>> print([var.name for var in problem.variables])
78
        ['x1', 'x2', 'x3']
79
        >>> optimizer = CplexOptimizer()
80
        >>> result = optimizer.solve(problem)
81
        >>> print(result.variable_names)
82
        ['x1', 'x2', 'x3']
83
        >>> print(result.x)
84
        [0. 1. 0.]
85
        >>> print(result[1])
86
        1.0
87
        >>> print(result['x1'])
88
        0.0
89
        >>> print(result.fval)
90
        -2.0
91
        >>> print(result.variables_dict)
92
        {'x1': 0.0, 'x2': 1.0, 'x3': 0.0}
93

94
    Note:
95
        The order of variables should be equal to that of the problem solved by
96
        optimization algorithms. Optimization algorithms and converters of ``QuadraticProgram``
97
        should maintain the order when generating a new ``OptimizationResult`` object.
98
    """
99

100
    def __init__(  # pylint: disable=too-many-positional-arguments
1✔
101
        self,
102
        x: Union[List[float], np.ndarray] | None,
103
        fval: float | None,
104
        variables: List[Variable],
105
        status: OptimizationResultStatus,
106
        raw_results: Any | None = None,
107
        samples: List[SolutionSample] | None = None,
108
    ) -> None:
109
        """
110
        Args:
111
            x: the variable values found in the optimization, or possibly None in case of FAILURE.
112
            fval: the objective function value.
113
            variables: the list of variables of the optimization problem.
114
            raw_results: the original results object from the optimization algorithm.
115
            status: the termination status of the optimization algorithm.
116
            samples: the solution samples.
117

118
        Raises:
119
            QiskitOptimizationError: if sizes of ``x`` and ``variables`` do not match.
120
        """
121
        self._variables = variables
1✔
122
        self._variable_names = [var.name for var in self._variables]
1✔
123
        if x is None:
1✔
124
            # if no state is given, it is set to None
125
            self._x = None  # pylint: disable=invalid-name
1✔
126
            self._variables_dict = None
1✔
127
        else:
128
            if len(x) != len(variables):
1✔
129
                raise QiskitOptimizationError(
×
130
                    f"Inconsistent size of variable values (x) and variables. x: size {len(x)} {x}, "
131
                    f"variables: size {len(variables)} {[v.name for v in variables]}"
132
                )
133
            self._x = np.asarray(x)
1✔
134
            self._variables_dict = {
1✔
135
                name: val.item() for name, val in zip(self._variable_names, self._x)
136
            }
137

138
        self._fval = fval
1✔
139
        self._raw_results = raw_results
1✔
140
        self._status = status
1✔
141
        if samples:
1✔
142
            sum_prob = np.sum([e.probability for e in samples])
1✔
143
            if not np.isclose(sum_prob, 1.0):
1✔
144
                logger.debug("The sum of probability of samples is not close to 1: %f", sum_prob)
×
145
            self._samples = samples
1✔
146
        else:
147
            self._samples = [
1✔
148
                SolutionSample(x=cast(np.ndarray, x), fval=fval, status=status, probability=1.0)
149
            ]
150

151
    def __repr__(self) -> str:
1✔
152
        return f"<{self.__class__.__name__}: {str(self)}>"
1✔
153

154
    def __str__(self) -> str:
1✔
155
        variables = ", ".join([f"{var}={x}" for var, x in self._variables_dict.items()])
1✔
156
        return f"fval={self._fval}, {variables}, status={self._status.name}"
1✔
157

158
    def prettyprint(self) -> str:
1✔
159
        """Returns a pretty printed string of this optimization result.
160

161
        Returns:
162
            A pretty printed string representing the result.
163
        """
164
        variables = ", ".join([f"{var}={x}" for var, x in self._variables_dict.items()])
1✔
165
        return (
1✔
166
            f"objective function value: {self._fval}\n"
167
            f"variable values: {variables}\n"
168
            f"status: {self._status.name}"
169
        )
170

171
    def __getitem__(self, key: Union[int, str]) -> float:
1✔
172
        """Returns the value of the variable whose index or name is equal to ``key``.
173

174
        The key can be an integer or a string.
175
        If the key is an integer, this methods returns the value of the variable
176
        whose index is equal to ``key``.
177
        If the key is a string, this methods return the value of the variable
178
        whose name is equal to ``key``.
179

180
        Args:
181
            key: an integer or a string.
182

183
        Returns:
184
            The value of a variable whose index or name is equal to ``key``.
185

186
        Raises:
187
            IndexError: if ``key`` is an integer and is out of range of the variables.
188
            KeyError: if ``key`` is a string and none of the variables has ``key`` as name.
189
            TypeError: if ``key`` is neither an integer nor a string.
190
        """
191
        if isinstance(key, int):
1✔
192
            return self._x[key]
1✔
193
        if isinstance(key, str):
×
194
            return self._variables_dict[key]
×
195
        raise TypeError(f"Integer or string key required, instead {type(key)}({key}) provided.")
×
196

197
    def get_correlations(self) -> np.ndarray:
1✔
198
        """
199
        Get <Zi x Zj> correlation matrix from the samples.
200

201
        Returns:
202
            A correlation matrix.
203
        """
204

205
        states = [v.x for v in self.samples]
1✔
206
        probs = [v.probability for v in self.samples]
1✔
207

208
        n = len(states[0])
1✔
209
        correlations = np.zeros((n, n))
1✔
210
        for k, prob in enumerate(probs):
1✔
211
            b = states[k]
1✔
212
            for i in range(n):
1✔
213
                for j in range(i):
1✔
214
                    if b[i] == b[j]:
1✔
215
                        correlations[i, j] += prob
1✔
216
                    else:
217
                        correlations[i, j] -= prob
1✔
218
        return correlations
1✔
219

220
    @property
1✔
221
    def x(self) -> np.ndarray | None:
1✔
222
        """Returns the variable values found in the optimization or None in case of FAILURE.
223

224
        Returns:
225
            The variable values found in the optimization.
226
        """
227
        return self._x
1✔
228

229
    @property
1✔
230
    def fval(self) -> float | None:
1✔
231
        """Returns the objective function value.
232

233
        Returns:
234
            The function value corresponding to the objective function value found in the optimization.
235
        """
236
        return self._fval
1✔
237

238
    @property
1✔
239
    def raw_results(self) -> Any:
1✔
240
        """Return the original results object from the optimization algorithm.
241

242
        Currently a dump for any leftovers.
243

244
        Returns:
245
            Additional result information of the optimization algorithm.
246
        """
247
        return self._raw_results
1✔
248

249
    @property
1✔
250
    def status(self) -> OptimizationResultStatus:
1✔
251
        """Returns the termination status of the optimization algorithm.
252

253
        Returns:
254
            The termination status of the algorithm.
255
        """
256
        return self._status
1✔
257

258
    @property
1✔
259
    def variables(self) -> List[Variable]:
1✔
260
        """Returns the list of variables of the optimization problem.
261

262
        Returns:
263
            The list of variables.
264
        """
265
        return self._variables
1✔
266

267
    @property
1✔
268
    def variables_dict(self) -> Dict[str, float]:
1✔
269
        """Returns the variable values as a dictionary of the variable name and corresponding value.
270

271
        Returns:
272
            The variable values as a dictionary of the variable name and corresponding value.
273
        """
274
        return self._variables_dict
×
275

276
    @property
1✔
277
    def variable_names(self) -> List[str]:
1✔
278
        """Returns the list of variable names of the optimization problem.
279

280
        Returns:
281
            The list of variable names of the optimization problem.
282
        """
283
        return self._variable_names
×
284

285
    @property
1✔
286
    def samples(self) -> List[SolutionSample]:
1✔
287
        """Returns the list of solution samples
288

289
        Returns:
290
            The list of solution samples.
291
        """
292
        return self._samples
1✔
293

294

295
class OptimizationAlgorithm(ABC):
1✔
296
    """An abstract class for optimization algorithms in Qiskit optimization module."""
297

298
    _MIN_PROBABILITY = 1e-6
1✔
299

300
    @abstractmethod
1✔
301
    def get_compatibility_msg(self, problem: QuadraticProgram) -> str:
1✔
302
        """Checks whether a given problem can be solved with the optimizer implementing this method.
303

304
        Args:
305
            problem: The optimization problem to check compatibility.
306

307
        Returns:
308
            Returns the incompatibility message. If the message is empty no issues were found.
309
        """
310

311
    def is_compatible(self, problem: QuadraticProgram) -> bool:
1✔
312
        """Checks whether a given problem can be solved with the optimizer implementing this method.
313

314
        Args:
315
            problem: The optimization problem to check compatibility.
316

317
        Returns:
318
            Returns True if the problem is compatible, False otherwise.
319
        """
320
        return len(self.get_compatibility_msg(problem)) == 0
×
321

322
    @abstractmethod
1✔
323
    def solve(self, problem: QuadraticProgram) -> "OptimizationResult":
1✔
324
        """Tries to solves the given problem using the optimizer.
325

326
        Runs the optimizer to try to solve the optimization problem.
327

328
        Args:
329
            problem: The problem to be solved.
330

331
        Returns:
332
            The result of the optimizer applied to the problem.
333

334
        Raises:
335
            QiskitOptimizationError: If the problem is incompatible with the optimizer.
336
        """
337
        raise NotImplementedError
×
338

339
    def _verify_compatibility(self, problem: QuadraticProgram) -> None:
1✔
340
        """Verifies that the problem is suitable for this optimizer. If the problem is not
341
        compatible then an exception is raised. This method is for convenience for concrete
342
        optimizers and is not intended to be used by end user.
343

344
        Args:
345
            problem: Problem to verify.
346

347
        Returns:
348
            None
349

350
        Raises:
351
            QiskitOptimizationError: If the problem is incompatible with the optimizer.
352

353
        """
354
        # check compatibility and raise exception if incompatible
355
        msg = self.get_compatibility_msg(problem)
1✔
356
        if msg:
1✔
357
            raise QiskitOptimizationError(f"Incompatible problem: {msg}")
1✔
358

359
    @staticmethod
1✔
360
    def _get_feasibility_status(
1✔
361
        problem: QuadraticProgram, x: Union[List[float], np.ndarray]
362
    ) -> OptimizationResultStatus:
363
        """Returns whether the input result is feasible or not for the given problem.
364

365
        Args:
366
            problem: Problem to verify.
367
            x: the input result list.
368

369
        Returns:
370
            The status of the result.
371
        """
372
        is_feasible = problem.is_feasible(x)
1✔
373

374
        return (
1✔
375
            OptimizationResultStatus.SUCCESS if is_feasible else OptimizationResultStatus.INFEASIBLE
376
        )
377

378
    @staticmethod
1✔
379
    def _prepare_converters(
1✔
380
        converters: Union[QuadraticProgramConverter, List[QuadraticProgramConverter]] | None,
381
        penalty: float | None = None,
382
    ) -> List[QuadraticProgramConverter]:
383
        """Prepare a list of converters from the input.
384

385
        Args:
386
            converters: The converters to use for converting a problem into a different form.
387
                By default, when None is specified, an internally created instance of
388
                :class:`~qiskit_optimization.converters.QuadraticProgramToQubo` will be used.
389
            penalty: The penalty factor used in the default
390
                :class:`~qiskit_optimization.converters.QuadraticProgramToQubo` converter
391

392
        Returns:
393
            The list of converters.
394

395
        Raises:
396
            TypeError: When the converters include those that are not
397
            :class:`~qiskit_optimization.converters.QuadraticProgramConverter type.
398
        """
399
        if converters is None:
1✔
400
            return [QuadraticProgramToQubo(penalty=penalty)]
1✔
401
        elif isinstance(converters, QuadraticProgramConverter):
1✔
402
            return [converters]
1✔
403
        elif isinstance(converters, list) and all(
1✔
404
            isinstance(converter, QuadraticProgramConverter) for converter in converters
405
        ):
406
            return converters
1✔
407
        else:
408
            raise TypeError("`converters` must all be of the QuadraticProgramConverter type")
1✔
409

410
    @staticmethod
1✔
411
    def _convert(
1✔
412
        problem: QuadraticProgram,
413
        converters: Union[QuadraticProgramConverter, List[QuadraticProgramConverter]],
414
    ) -> QuadraticProgram:
415
        """Convert the problem with the converters
416

417
        Args:
418
            problem: The problem to be solved
419
            converters: The converters to use for converting a problem into a different form.
420

421
        Returns:
422
            The problem converted by the converters.
423
        """
424
        problem_ = problem
1✔
425

426
        if not isinstance(converters, list):
1✔
427
            converters = [converters]
1✔
428

429
        for converter in converters:
1✔
430
            problem_ = converter.convert(problem_)
1✔
431

432
        return problem_
1✔
433

434
    @staticmethod
1✔
435
    def _check_converters(
1✔
436
        converters: Union[QuadraticProgramConverter, List[QuadraticProgramConverter]] | None,
437
    ) -> List[QuadraticProgramConverter]:
438
        if converters is None:
1✔
439
            converters = []
1✔
440
        if not isinstance(converters, list):
1✔
441
            converters = [converters]
1✔
442
        if not all(isinstance(conv, QuadraticProgramConverter) for conv in converters):
1✔
443
            raise TypeError(f"Invalid object of converters: {converters}")
×
444
        return converters
1✔
445

446
    @classmethod
1✔
447
    def _interpret(
1✔
448
        cls,
449
        x: np.ndarray,
450
        problem: QuadraticProgram,
451
        converters: Union[QuadraticProgramConverter, List[QuadraticProgramConverter]] | None = None,
452
        result_class: Type[OptimizationResult] = OptimizationResult,
453
        **kwargs,
454
    ) -> OptimizationResult:
455
        """Convert back the result of the converted problem to the result of the original problem.
456

457
        Args:
458
            x: The result of the converted problem.
459
            converters: The converters to use for converting back the result of the problem
460
                to the result of the original problem.
461
            problem: The original problem for which `x` is interpreted.
462
            result_class: The class of the result object.
463
            kwargs: parameters of the constructor of result_class
464

465
        Returns:
466
            The result of the original problem.
467

468
        Raises:
469
            QiskitOptimizationError: if result_class is not a sub-class of OptimizationResult.
470
            TypeError: if converters are not QuadraticProgramConverter or a list of
471
                QuadraticProgramConverter.
472
        """
473
        if not issubclass(result_class, OptimizationResult):
1✔
474
            raise QiskitOptimizationError(
×
475
                f"Invalid result class, not derived from OptimizationResult: {result_class}"
476
            )
477
        converters = cls._check_converters(converters)
1✔
478

479
        for converter in converters[::-1]:
1✔
480
            x = converter.interpret(x)
1✔
481
        return result_class(
1✔
482
            x=x,
483
            fval=problem.objective.evaluate(x),
484
            variables=problem.variables,
485
            status=cls._get_feasibility_status(problem, x),
486
            **kwargs,
487
        )
488

489
    @classmethod
1✔
490
    def _interpret_samples(
1✔
491
        cls,
492
        problem: QuadraticProgram,
493
        raw_samples: List[SolutionSample],
494
        converters: QuadraticProgramConverter | list[QuadraticProgramConverter] | None = None,
495
    ) -> Tuple[List[SolutionSample], SolutionSample]:
496
        """Interpret and sort all samples and return the raw sample corresponding to the best one"""
497
        converters = cls._check_converters(converters)
1✔
498

499
        prob: Dict[Tuple, float] = {}
1✔
500
        array = {}
1✔
501
        index = {}
1✔
502
        for i, sample in enumerate(raw_samples):
1✔
503
            x = sample.x
1✔
504
            for converter in converters[::-1]:
1✔
505
                x = converter.interpret(x)
1✔
506
            key = tuple(x)
1✔
507
            prob[key] = prob.get(key, 0.0) + sample.probability
1✔
508
            array[key] = x
1✔
509
            index[key] = i
1✔
510

511
        samples = []
1✔
512
        for key, x in array.items():
1✔
513
            probability = prob[key]
1✔
514
            fval = problem.objective.evaluate(x)
1✔
515
            status = cls._get_feasibility_status(problem, x)
1✔
516
            samples.append(SolutionSample(x, fval, probability, status))
1✔
517

518
        sorted_samples = sorted(
1✔
519
            samples,
520
            key=lambda v: (v.status.value, problem.objective.sense.value * v.fval),
521
        )
522
        best_raw = raw_samples[index[tuple(sorted_samples[0].x)]]
1✔
523
        return sorted_samples, best_raw
1✔
524

525
    @staticmethod
1✔
526
    def _eigenvector_to_solutions(
1✔
527
        eigenvector: Union[QuasiDistribution, Statevector, dict, np.ndarray],
528
        qubo: QuadraticProgram,
529
        min_probability: float = _MIN_PROBABILITY,
530
    ) -> List[SolutionSample]:
531
        """Convert the eigenvector to the bitstrings and corresponding eigenvalues.
532

533
        Args:
534
            eigenvector: The eigenvector from which the solution states are extracted.
535
            qubo: The QUBO to evaluate at the bitstring.
536
            min_probability: Only consider states where the amplitude exceeds this threshold.
537

538
        Returns:
539
            For each computational basis state contained in the eigenvector, return the basis
540
            state as bitstring along with the QUBO evaluated at that bitstring and the
541
            probability of sampling this bitstring from the eigenvector.
542

543
        Raises:
544
            TypeError: If the type of eigenvector is not supported.
545
        """
546

547
        def generate_solution(bitstr, qubo, probability):
1✔
548
            x = np.fromiter(list(bitstr[::-1]), dtype=int)
1✔
549
            fval = qubo.objective.evaluate(x)
1✔
550
            return SolutionSample(
1✔
551
                x=x,
552
                fval=fval,
553
                probability=probability,
554
                status=OptimizationResultStatus.SUCCESS,
555
            )
556

557
        solutions = []
1✔
558
        if isinstance(eigenvector, QuasiDistribution):
1✔
559
            probabilities = eigenvector.binary_probabilities()
1✔
560
            # iterate over all samples
561
            for bitstr, sampling_probability in probabilities.items():
1✔
562
                # add the bitstring, if the sampling probability exceeds the threshold
563
                if sampling_probability >= min_probability:
1✔
564
                    solutions.append(generate_solution(bitstr, qubo, sampling_probability))
1✔
565

566
        elif isinstance(eigenvector, Statevector):
1✔
567
            probabilities = eigenvector.probabilities()
1✔
568
            num_qubits = eigenvector.num_qubits
1✔
569
            # iterate over all states and their sampling probabilities
570
            for i, sampling_probability in enumerate(probabilities):
1✔
571
                # add the i-th state if the sampling probability exceeds the threshold
572
                if sampling_probability >= min_probability:
1✔
573
                    bitstr = f"{i:b}".rjust(num_qubits, "0")
1✔
574
                    solutions.append(generate_solution(bitstr, qubo, sampling_probability))
1✔
575

576
        elif isinstance(eigenvector, dict):
1✔
577
            # When eigenvector is a dict, square the values since the values are normalized.
578
            # See https://github.com/Qiskit/qiskit-terra/pull/5496 for more details.
579
            probabilities = {bitstr: val**2 for (bitstr, val) in eigenvector.items()}
1✔
580
            # iterate over all samples
581
            for bitstr, sampling_probability in probabilities.items():
1✔
582
                # add the bitstring, if the sampling probability exceeds the threshold
583
                if sampling_probability >= min_probability:
1✔
584
                    solutions.append(generate_solution(bitstr, qubo, sampling_probability))
1✔
585

586
        elif isinstance(eigenvector, np.ndarray):
×
587
            num_qubits = int(np.log2(eigenvector.size))
×
588
            probabilities = np.abs(eigenvector * eigenvector.conj())
×
589
            # iterate over all states and their sampling probabilities
590
            for i, sampling_probability in enumerate(probabilities):
×
591
                # add the i-th state if the sampling probability exceeds the threshold
592
                if sampling_probability >= min_probability:
×
593
                    bitstr = f"{i:b}".rjust(num_qubits, "0")
×
594
                    solutions.append(generate_solution(bitstr, qubo, sampling_probability))
×
595

596
        else:
597
            raise TypeError(
×
598
                f"Eigenvector should be QuasiDistribution, Statevector, dict or numpy.ndarray. "
599
                f"But, it was {type(eigenvector)}."
600
            )
601
        return solutions
1✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc