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

qiskit-community / qiskit-optimization / 6032830758

31 Aug 2023 03:52AM CUT coverage: 93.022% (+0.6%) from 92.416%
6032830758

Pull #542

github

web-flow
Merge 0c24ac100 into e2855f2b6
Pull Request #542: Remove runtime program, quantum instance, and opflow

27 of 27 new or added lines in 7 files covered. (100.0%)

4146 of 4457 relevant lines covered (93.02%)

0.93 hits per line

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

90.15
/qiskit_optimization/algorithms/optimization_algorithm.py
1
# This code is part of Qiskit.
2
#
3
# (C) Copyright IBM 2020, 2023.
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's optimization module."""
1✔
14

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

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

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

29
logger = getLogger(__name__)
1✔
30

31

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

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

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

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

44

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

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

58

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

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

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

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

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

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

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

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

148
    def __repr__(self) -> str:
1✔
149
        return f"<{self.__class__.__name__}: {str(self)}>"
1✔
150

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

155
    def prettyprint(self) -> str:
1✔
156
        """Returns a pretty printed string of this optimization result.
157

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

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

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

177
        Args:
178
            key: an integer or a string.
179

180
        Returns:
181
            The value of a variable whose index or name is equal to ``key``.
182

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

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

198
        Returns:
199
            A correlation matrix.
200
        """
201

202
        states = [v.x for v in self.samples]
1✔
203
        probs = [v.probability for v in self.samples]
1✔
204

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

217
    @property
1✔
218
    def x(self) -> Optional[np.ndarray]:
1✔
219
        """Returns the variable values found in the optimization or None in case of FAILURE.
220

221
        Returns:
222
            The variable values found in the optimization.
223
        """
224
        return self._x
1✔
225

226
    @property
1✔
227
    def fval(self) -> Optional[float]:
1✔
228
        """Returns the objective function value.
229

230
        Returns:
231
            The function value corresponding to the objective function value found in the optimization.
232
        """
233
        return self._fval
1✔
234

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

239
        Currently a dump for any leftovers.
240

241
        Returns:
242
            Additional result information of the optimization algorithm.
243
        """
244
        return self._raw_results
1✔
245

246
    @property
1✔
247
    def status(self) -> OptimizationResultStatus:
1✔
248
        """Returns the termination status of the optimization algorithm.
249

250
        Returns:
251
            The termination status of the algorithm.
252
        """
253
        return self._status
1✔
254

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

259
        Returns:
260
            The list of variables.
261
        """
262
        return self._variables
1✔
263

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

268
        Returns:
269
            The variable values as a dictionary of the variable name and corresponding value.
270
        """
271
        return self._variables_dict
×
272

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

277
        Returns:
278
            The list of variable names of the optimization problem.
279
        """
280
        return self._variable_names
×
281

282
    @property
1✔
283
    def samples(self) -> List[SolutionSample]:
1✔
284
        """Returns the list of solution samples
285

286
        Returns:
287
            The list of solution samples.
288
        """
289
        return self._samples
1✔
290

291

292
class OptimizationAlgorithm(ABC):
1✔
293
    """An abstract class for optimization algorithms in Qiskit's optimization module."""
294

295
    _MIN_PROBABILITY = 1e-6
1✔
296

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

301
        Args:
302
            problem: The optimization problem to check compatibility.
303

304
        Returns:
305
            Returns the incompatibility message. If the message is empty no issues were found.
306
        """
307

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

311
        Args:
312
            problem: The optimization problem to check compatibility.
313

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

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

323
        Runs the optimizer to try to solve the optimization problem.
324

325
        Args:
326
            problem: The problem to be solved.
327

328
        Returns:
329
            The result of the optimizer applied to the problem.
330

331
        Raises:
332
            QiskitOptimizationError: If the problem is incompatible with the optimizer.
333
        """
334
        raise NotImplementedError
×
335

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

341
        Args:
342
            problem: Problem to verify.
343

344
        Returns:
345
            None
346

347
        Raises:
348
            QiskitOptimizationError: If the problem is incompatible with the optimizer.
349

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

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

362
        Args:
363
            problem: Problem to verify.
364
            x: the input result list.
365

366
        Returns:
367
            The status of the result.
368
        """
369
        is_feasible = problem.is_feasible(x)
1✔
370

371
        return (
1✔
372
            OptimizationResultStatus.SUCCESS if is_feasible else OptimizationResultStatus.INFEASIBLE
373
        )
374

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

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

389
        Returns:
390
            The list of converters.
391

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

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

414
        Args:
415
            problem: The problem to be solved
416
            converters: The converters to use for converting a problem into a different form.
417

418
        Returns:
419
            The problem converted by the converters.
420
        """
421
        problem_ = problem
1✔
422

423
        if not isinstance(converters, list):
1✔
424
            converters = [converters]
1✔
425

426
        for converter in converters:
1✔
427
            problem_ = converter.convert(problem_)
1✔
428

429
        return problem_
1✔
430

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

595
        else:
596
            raise TypeError(
×
597
                f"Eigenvector should be QuasiDistribution, Statevector, dict or numpy.ndarray. "
598
                f"But, it was {type(eigenvector)}."
599
            )
600
        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

© 2025 Coveralls, Inc