Skip to content

Commit 0a10e39

Browse files
authored
optimize: have linprog return a specialized OptimizeResult (#868)
2 parents a308cab + fa7340e commit 0a10e39

File tree

3 files changed

+59
-35
lines changed

3 files changed

+59
-35
lines changed

scipy-stubs/optimize/_linprog.pyi

Lines changed: 45 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
from collections.abc import Callable, Sequence
2-
from typing import Final, Literal, LiteralString, TypeAlias, TypedDict, overload, type_check_only
2+
from typing import Final, Literal, TypeAlias, TypedDict, overload, type_check_only
33
from typing_extensions import deprecated
44

55
import numpy as np
@@ -15,11 +15,9 @@ __all__ = ["linprog", "linprog_terse_callback", "linprog_verbose_callback"]
1515

1616
_Ignored: TypeAlias = object
1717
_Max3: TypeAlias = Literal[0, 1, 2, 3]
18-
_Max4: TypeAlias = Literal[_Max3, 4]
1918

2019
_Int: TypeAlias = int | np.int32 | np.int64
2120
_Float: TypeAlias = float | np.float64
22-
_Float1D: TypeAlias = onp.Array1D[np.float64]
2321

2422
@type_check_only
2523
class _OptionsCommon(TypedDict, total=False):
@@ -79,21 +77,39 @@ class _OptionsSimplex(_OptionsCommonLegacy, TypedDict, total=False):
7977

8078
###
8179

80+
class OptimizeResult(_OptimizeResult):
81+
x: onp.Array1D[np.float64] | None
82+
fun: float | None
83+
slack: onp.Array1D[np.float64] | None
84+
con: onp.Array1D[np.float64] | None
85+
success: bool
86+
status: Literal[0, 1, 2, 3, 4]
87+
message: str
88+
nit: int
89+
90+
@type_check_only
91+
class _OptimizeResultSensitivity(_OptimizeResult):
92+
residual: onp.Array1D[np.float64]
93+
marginals: onp.Array1D[np.float64]
94+
95+
@type_check_only
96+
class _OptimizeResultHighs(OptimizeResult):
97+
crossover_nit: int
98+
lower: _OptimizeResultSensitivity
99+
upper: _OptimizeResultSensitivity
100+
eqlin: _OptimizeResultSensitivity
101+
ineqlin: _OptimizeResultSensitivity
102+
mip_node_count: int # only exists if `success=True`
103+
mip_dual_bound: float # only exists if `success=True`
104+
mip_gap: float # only exists if `success=True`
105+
106+
###
107+
82108
__docformat__: Final = "restructuredtext en" # undocumented
83109
LINPROG_METHODS: Final[Sequence[MethodLinprog | MethodLinprogLegacy]] = ... # undocumented
84110

85-
class OptimizeResult(_OptimizeResult):
86-
x: _Float1D # minimizing decision variables w.r.t. the constraints
87-
fun: _Float # optimal objective function value
88-
slack: _Float1D # slack values; nominally positive
89-
con: _Float1D # residuals of equality constraints; nominally zero
90-
status: _Max4
91-
message: LiteralString
92-
nit: int # >=0
93-
success: bool # `success = status == 0`
94-
95-
def linprog_verbose_callback(res: _OptimizeResult) -> None: ...
96-
def linprog_terse_callback(res: _OptimizeResult) -> None: ...
111+
def linprog_verbose_callback(res: OptimizeResult) -> None: ...
112+
def linprog_terse_callback(res: OptimizeResult) -> None: ...
97113

98114
#
99115
@overload # highs (default)
@@ -105,11 +121,11 @@ def linprog(
105121
b_eq: onp.ToFloat1D | None = None,
106122
bounds: Bound | Sequence[Bound] = (0, None),
107123
method: Literal["highs"] = "highs",
108-
callback: Callable[[_OptimizeResult], _Ignored] | None = None,
124+
callback: Callable[[OptimizeResult], _Ignored] | None = None,
109125
options: _OptionsHighs | None = None,
110126
x0: onp.ToFloat1D | None = None,
111127
integrality: _Max3 | Sequence[_Max3] | onp.CanArrayND[npc.integer] | None = None,
112-
) -> _OptimizeResult: ...
128+
) -> _OptimizeResultHighs: ...
113129
@overload # highs-ds
114130
def linprog(
115131
c: onp.ToFloat1D,
@@ -120,11 +136,11 @@ def linprog(
120136
bounds: Bound | Sequence[Bound] = (0, None),
121137
*,
122138
method: Literal["highs-ds"],
123-
callback: Callable[[_OptimizeResult], _Ignored] | None = None,
139+
callback: Callable[[OptimizeResult], _Ignored] | None = None,
124140
options: _OptionsHighsDS | None = None,
125141
x0: onp.ToFloat1D | None = None,
126142
integrality: _Max3 | Sequence[_Max3] | onp.CanArrayND[npc.integer] | None = None,
127-
) -> _OptimizeResult: ...
143+
) -> _OptimizeResultHighs: ...
128144
@overload # highs-ipm
129145
def linprog(
130146
c: onp.ToFloat1D,
@@ -135,11 +151,11 @@ def linprog(
135151
bounds: Bound | Sequence[Bound] = (0, None),
136152
*,
137153
method: Literal["highs-ipm"],
138-
callback: Callable[[_OptimizeResult], _Ignored] | None = None,
154+
callback: Callable[[OptimizeResult], _Ignored] | None = None,
139155
options: _OptionsHighsIPM | None = None,
140156
x0: onp.ToFloat1D | None = None,
141157
integrality: _Max3 | Sequence[_Max3] | onp.CanArrayND[npc.integer] | None = None,
142-
) -> _OptimizeResult: ...
158+
) -> _OptimizeResultHighs: ...
143159
@overload # interior-point (legacy, see https://github.com/scipy/scipy/issues/15707)
144160
@deprecated("`method='interior-point'` is deprecated and will be removed in SciPy 1.17. Please use one of the HIGHS solvers.")
145161
def linprog(
@@ -151,11 +167,11 @@ def linprog(
151167
bounds: Bound | Sequence[Bound] = (0, None),
152168
*,
153169
method: Literal["interior-point"],
154-
callback: Callable[[_OptimizeResult], _Ignored] | None = None,
170+
callback: Callable[[OptimizeResult], _Ignored] | None = None,
155171
options: _OptionsInteriorPoint | None = None,
156172
x0: onp.ToFloat1D | None = None,
157173
integrality: _Max3 | Sequence[_Max3] | onp.CanArrayND[npc.integer] | None = None,
158-
) -> _OptimizeResult: ...
174+
) -> OptimizeResult: ...
159175
@overload # revised simplex (legacy, see https://github.com/scipy/scipy/issues/15707)
160176
@deprecated("`method='revised simplex'` is deprecated and will be removed in SciPy 1.17. Please use one of the HIGHS solvers.")
161177
def linprog(
@@ -167,11 +183,11 @@ def linprog(
167183
bounds: Bound | Sequence[Bound] = (0, None),
168184
*,
169185
method: Literal["revised simplex"],
170-
callback: Callable[[_OptimizeResult], _Ignored] | None = None,
186+
callback: Callable[[OptimizeResult], _Ignored] | None = None,
171187
options: _OptionsRevisedSimplex | None = None,
172188
x0: onp.ToFloat1D | None = None,
173189
integrality: _Max3 | Sequence[_Max3] | onp.CanArrayND[npc.integer] | None = None,
174-
) -> _OptimizeResult: ...
190+
) -> OptimizeResult: ...
175191
@overload # simplex (legacy, see https://github.com/scipy/scipy/issues/15707)
176192
@deprecated("`method='simplex'` is deprecated and will be removed in SciPy 1.17. Please use one of the HIGHS solvers.")
177193
def linprog(
@@ -183,11 +199,11 @@ def linprog(
183199
bounds: Bound | Sequence[Bound] = (0, None),
184200
*,
185201
method: Literal["simplex"],
186-
callback: Callable[[_OptimizeResult], _Ignored] | None = None,
202+
callback: Callable[[OptimizeResult], _Ignored] | None = None,
187203
options: _OptionsSimplex | None = None,
188204
x0: onp.ToFloat1D | None = None,
189205
integrality: _Max3 | Sequence[_Max3] | onp.CanArrayND[npc.integer] | None = None,
190-
) -> _OptimizeResult: ...
206+
) -> OptimizeResult: ...
191207
@overload # any "highs"
192208
def linprog(
193209
c: onp.ToFloat1D,
@@ -197,8 +213,8 @@ def linprog(
197213
b_eq: onp.ToFloat1D | None = None,
198214
bounds: Bound | Sequence[Bound] = (0, None),
199215
method: MethodLinprog = "highs",
200-
callback: Callable[[_OptimizeResult], _Ignored] | None = None,
216+
callback: Callable[[OptimizeResult], _Ignored] | None = None,
201217
options: _OptionsHighs | None = None,
202218
x0: onp.ToFloat1D | None = None,
203219
integrality: _Max3 | Sequence[_Max3] | onp.CanArrayND[npc.integer] | None = None,
204-
) -> _OptimizeResult: ...
220+
) -> _OptimizeResultHighs: ...

tests/.ruff.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
extend = "../pyproject.toml"
22

33
[lint]
4-
extend-ignore = ["B018", "PYI015", "PYI017"]
4+
extend-ignore = ["B018", "PYI002", "PYI015", "PYI017"]

tests/optimize/test_linprog.pyi

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,23 @@
11
from collections.abc import Sequence
22
from typing import assert_type
33

4+
import numpy as np
5+
46
from scipy.optimize import OptimizeResult, linprog
57
from scipy.optimize._typing import Bound
68

79
c: list[float]
810

911
bound: Bound
10-
uniformly_bounded = linprog(c, bounds=bound)
11-
assert_type(uniformly_bounded, OptimizeResult)
12-
1312
bounds: Sequence[Bound]
14-
variably_bounded = linprog(c, bounds=bounds)
15-
assert_type(variably_bounded, OptimizeResult)
13+
14+
###
15+
16+
res = linprog(c, bounds=bounds)
17+
assert_type(res.fun, float | None)
18+
assert_type(res.x, np.ndarray[tuple[int], np.dtype[np.float64]] | None)
19+
assert_type(res.success, bool)
20+
assert_type(res.message, str)
21+
22+
_1: OptimizeResult = linprog(c, bounds=bound)
23+
_2: OptimizeResult = linprog(c, bounds=bounds)

0 commit comments

Comments
 (0)