3232
3333"""
3434
35- from __future__ import annotations
36-
3735from dataclasses import dataclass
3836
3937import numpy as np
38+ from scipy .optimize import linprog # type: ignore[import-untyped]
4039
4140# Constants
4241EXPECTED_MATRIX_DIMENSIONS = 2
4342
44- try :
45- # SciPy is used for linear programming; HiGHS is fast and reliable.
46- from scipy .optimize import linprog # type: ignore
47- except Exception : # pragma: no cover
48- linprog = None
49-
5043
5144@dataclass
5245class CalibrationResult :
@@ -66,6 +59,7 @@ class CalibrationResult:
6659 Status code from the linear programme (0 indicates success).
6760 message : str
6861 Solver termination message for diagnostics.
62+
6963 """
7064
7165 w : np .ndarray
@@ -76,7 +70,7 @@ class CalibrationResult:
7670
7771
7872def _validate_inputs (
79- A : np .ndarray , b : np .ndarray , w0 : np .ndarray
73+ A : np .ndarray , b : np .ndarray , w0 : np .ndarray ,
8074) -> tuple [np .ndarray , np .ndarray , np .ndarray ]:
8175 """Validate and coerce input arrays to ensure they have compatible shapes.
8276
@@ -98,17 +92,21 @@ def _validate_inputs(
9892 ------
9993 ValueError
10094 If shapes are incompatible.
95+
10196 """
10297 A = np .asarray (A , dtype = float )
10398 b = np .asarray (b , dtype = float )
10499 w0 = np .asarray (w0 , dtype = float )
105100 if A .ndim != EXPECTED_MATRIX_DIMENSIONS :
106- raise ValueError (f"A must be two-dimensional, got shape { A .shape } " )
101+ msg = f"A must be two-dimensional, got shape { A .shape } "
102+ raise ValueError (msg )
107103 m , n = A .shape
108104 if b .shape != (m ,):
109- raise ValueError (f"b must be of shape { (m ,)} , got { b .shape } " )
105+ msg = f"b must be of shape { (m ,)} , got { b .shape } "
106+ raise ValueError (msg )
110107 if w0 .shape != (n ,):
111- raise ValueError (f"w0 must be of shape { (n ,)} , got { w0 .shape } " )
108+ msg = f"w0 must be of shape { (n ,)} , got { w0 .shape } "
109+ raise ValueError (msg )
112110 return A , b , w0
113111
114112
@@ -138,11 +136,8 @@ def _solve_lp(
138136 -------
139137 res : OptimizeResult
140138 Result from the solver.
139+
141140 """
142- if linprog is None :
143- raise ImportError (
144- "SciPy is required to solve the linear programmes. Install scipy>=1.6 to use this function."
145- )
146141 res = linprog (
147142 c = c ,
148143 A_ub = A_ub ,
@@ -197,6 +192,7 @@ def leximin_residual(
197192 If the problem is infeasible (e.g., because the bounds preclude any
198193 solution), the returned status will be nonzero and the weights may not be
199194 meaningful. Check ``status`` and ``message`` on the result.
195+
200196 """
201197 A , b , w0 = _validate_inputs (A , b , w0 )
202198 m , n = A .shape
@@ -231,7 +227,7 @@ def leximin_residual(
231227 w = x [:n ]
232228 epsilon = x [- 1 ]
233229 return CalibrationResult (
234- w = w , epsilon = epsilon , t = None , status = res .status , message = res .message
230+ w = w , epsilon = epsilon , t = None , status = res .status , message = res .message ,
235231 )
236232
237233
@@ -255,6 +251,7 @@ def _setup_weight_fair_constraints(
255251 Inequality constraint right hand side.
256252 bounds : list
257253 Variable bounds.
254+
258255 """
259256 m , n = A .shape
260257
@@ -350,6 +347,7 @@ def leximin_weight_fair(
350347 :class:`CalibrationResult` containing the final weights and both the
351348 residual and weight fairness optima. If ``return_stages`` is
352349 ``True``, a tuple ``(stage1_result, stage2_result)``.
350+
353351 """
354352 stage1 = leximin_residual (A , b , w0 , min_ratio = min_ratio , max_ratio = max_ratio )
355353 # If the residual stage failed, propagate the failure
@@ -369,7 +367,7 @@ def leximin_weight_fair(
369367
370368 # Set up constraints using helper function
371369 A_ub , b_ub , bounds = _setup_weight_fair_constraints (
372- A , b , w0 , stage1 .epsilon , min_ratio = min_ratio , max_ratio = max_ratio , slack = slack
370+ A , b , w0 , stage1 .epsilon , min_ratio = min_ratio , max_ratio = max_ratio , slack = slack ,
373371 )
374372
375373 res = _solve_lp (c , A_ub , b_ub , bounds )
@@ -389,7 +387,7 @@ def leximin_weight_fair(
389387 w = x [:n ]
390388 t_opt = x [- 1 ]
391389 stage2 = CalibrationResult (
392- w = w , epsilon = stage1 .epsilon , t = t_opt , status = res .status , message = res .message
390+ w = w , epsilon = stage1 .epsilon , t = t_opt , status = res .status , message = res .message ,
393391 )
394392 if return_stages :
395393 return stage1 , stage2
0 commit comments