Coverage for moptipy / utils / math.py: 85%

128 statements  

« prev     ^ index     » next       coverage.py v7.12.0, created at 2025-11-24 08:49 +0000

1"""Simple routines for handling numbers and numerical stuff.""" 

2 

3import contextlib 

4from math import exp, inf, isfinite, isqrt, log, log2, nextafter 

5from typing import Final 

6 

7from pycommons.types import type_error 

8 

9#: The positive limit for doubles that can be represented exactly as ints. 

10DBL_INT_LIMIT_P: Final[float] = 9007199254740992.0 # = 1 << 53 

11#: The negative limit for doubles that can be represented exactly as ints. 

12_DBL_INT_LIMIT_N: Final[float] = -DBL_INT_LIMIT_P 

13 

14 

15def __try_int(val: float) -> int | float: 

16 """ 

17 Convert a float to an int without any fancy checks. 

18 

19 :param val: the flot 

20 :returns: the float or int 

21 

22 >>> from math import inf, nan, nextafter 

23 >>> type(__try_int(0.0)) 

24 <class 'int'> 

25 >>> type(__try_int(0.5)) 

26 <class 'float'> 

27 >>> type(__try_int(inf)) 

28 <class 'float'> 

29 >>> type(__try_int(-inf)) 

30 <class 'float'> 

31 >>> type(__try_int(nan)) 

32 <class 'float'> 

33 >>> 1 << 53 

34 9007199254740992 

35 >>> type(__try_int(9007199254740992.0)) 

36 <class 'int'> 

37 >>> __try_int(9007199254740992.0) 

38 9007199254740992 

39 >>> too_big = nextafter(9007199254740992.0, inf) 

40 >>> print(too_big) 

41 9007199254740994.0 

42 >>> type(__try_int(too_big)) 

43 <class 'float'> 

44 >>> type(__try_int(-9007199254740992.0)) 

45 <class 'int'> 

46 >>> __try_int(-9007199254740992.0) 

47 -9007199254740992 

48 >>> type(__try_int(-too_big)) 

49 <class 'float'> 

50 """ 

51 if _DBL_INT_LIMIT_N <= val <= DBL_INT_LIMIT_P: 

52 a = int(val) 

53 if a == val: 

54 return a 

55 return val 

56 

57 

58def try_int(val: int | float) -> int | float: 

59 """ 

60 Attempt to convert a float to an integer. 

61 

62 This method will convert a floating point number to an integer if the 

63 floating point number was representing an exact integer. This is the 

64 case if it has a) no fractional part and b) is in the range 

65 `-9007199254740992...9007199254740992`, i.e., the range where `+1` and 

66 `-1` work without loss of precision. 

67 

68 :param val: the input value, which must either be `int` or `float` 

69 :return: an `int` if `val` can be represented as `int` without loss of 

70 precision, `val` otherwise 

71 :raises TypeError: if `val` is neither an instance of `int` nor of `float` 

72 :raises ValueError: if `val` is a `float`, but not finite 

73 

74 >>> print(type(try_int(10.5))) 

75 <class 'float'> 

76 >>> print(type(try_int(10))) 

77 <class 'int'> 

78 

79 >>> from math import inf, nan, nextafter 

80 >>> type(try_int(0.0)) 

81 <class 'int'> 

82 >>> type(try_int(0.5)) 

83 <class 'float'> 

84 >>> try: 

85 ... try_int(inf) 

86 ... except ValueError as ve: 

87 ... print(ve) 

88 Value must be finite, but is inf. 

89 >>> try: 

90 ... try_int(-inf) 

91 ... except ValueError as ve: 

92 ... print(ve) 

93 Value must be finite, but is -inf. 

94 >>> try: 

95 ... try_int(nan) 

96 ... except ValueError as ve: 

97 ... print(ve) 

98 Value must be finite, but is nan. 

99 >>> try: 

100 ... try_int("blab") # noqa # type: off 

101 ... except TypeError as te: 

102 ... print(te) 

103 val should be an instance of any in {float, int} but is str, namely \ 

104'blab'. 

105 >>> type(try_int(9007199254740992.0)) 

106 <class 'int'> 

107 >>> try_int(9007199254740992.0) 

108 9007199254740992 

109 >>> too_big = nextafter(9007199254740992.0, inf) 

110 >>> print(too_big) 

111 9007199254740994.0 

112 >>> type(try_int(too_big)) 

113 <class 'float'> 

114 >>> type(try_int(-9007199254740992.0)) 

115 <class 'int'> 

116 >>> try_int(-9007199254740992.0) 

117 -9007199254740992 

118 >>> type(try_int(-too_big)) 

119 <class 'float'> 

120 """ 

121 if isinstance(val, int): 

122 return val 

123 if isinstance(val, float): 

124 if not isfinite(val): 

125 raise ValueError(f"Value must be finite, but is {val}.") 

126 if _DBL_INT_LIMIT_N <= val <= DBL_INT_LIMIT_P: 

127 a = int(val) 

128 if a == val: 

129 return a 

130 return val 

131 raise type_error(val, "val", (int, float)) 

132 

133 

134def try_int_div(a: int, b: int) -> int | float: 

135 """ 

136 Try to divide two integers at best precision. 

137 

138 Floating point divisions can incur some loss of precision. We try 

139 to avoid this here as much as possible. First, we check if `a` is 

140 divisible by `b` without any fractional part. If this is true, then 

141 we can do a pure integer division without loss of any precision. 

142 

143 Otherwise, we would do a floating point division. This will lead the 

144 values be converted to floating point numbers, which can incur 

145 a loss of precision. In the past, we tried to divide both numbers by 

146 their `gcd` to make them smaller in order to avoid a loss of precision. 

147 But it seems that Python is already doing something like this internally 

148 when performing the `a / b` division anyway, so we don't do this anymore. 

149 However, it is still attempted to convert the result back to an integer. 

150 

151 :param a: the first integer 

152 :param b: the second integer 

153 :return: a/b, either as `int` or as `float` but always a finite value 

154 :raises OverflowError: if the division must be performed using floating 

155 point arithmetic, but the result would be too large to fit into a 

156 `float` 

157 :raises ZeroDivisionError: if `b==0` 

158 

159 >>> print(try_int_div(10, 2)) 

160 5 

161 >>> print(try_int_div(10, -2)) 

162 -5 

163 >>> print(try_int_div(-10, 2)) 

164 -5 

165 >>> print(try_int_div(-10, -2)) 

166 5 

167 >>> print(type(try_int_div(10, 2))) 

168 <class 'int'> 

169 >>> print(try_int_div(10, 3)) 

170 3.3333333333333335 

171 >>> print(try_int_div(-10, 3)) 

172 -3.3333333333333335 

173 >>> print(try_int_div(10, -3)) 

174 -3.3333333333333335 

175 >>> print(try_int_div(-10, -3)) 

176 3.3333333333333335 

177 >>> print(type(try_int_div(10, 3))) 

178 <class 'float'> 

179 >>> print(try_int_div(9007199254740992, 1)) 

180 9007199254740992 

181 >>> print(try_int_div(2109792310235001520128, 234234)) 

182 9007199254740992 

183 >>> print(try_int_div(2109792310235001520128, 234235)) 

184 9007160801054503 

185 >>> print(try_int_div(2109792310235001520128, 234233)) 

186 9007237708755818.0 

187 >>> large = 123456789012345678901234567890123456789012345678901234567\ 

18889012345678901234567890123456789012345678901234567890123456789012345678901234\ 

18956789012345678901234567890123456789012345678901234567890123456789012345678901\ 

19023456789012345678901234567890123456789012345678901234567890123456789012345678\ 

19190123456789012345678901234567890123456789012345678901234567890123456789012345\ 

19267890123456789012345678901234567890123456789012345678901234567890123456789012\ 

1933456789012345678901234567890123456789012345678901234567890123456789012345678\ 

19490123456789012345678901234567890123456789012345678901234567890123456789012345\ 

195678901234567890123456789012345678901234567890 

196 >>> try: 

197 ... large / 1 

198 ... except OverflowError as oe: 

199 ... print(oe) 

200 integer division result too large for a float 

201 >>> try_int_div(large, 1) 

202 123456789012345678901234567890123456789012345678901234567\ 

20389012345678901234567890123456789012345678901234567890123456789012345678901234\ 

20456789012345678901234567890123456789012345678901234567890123456789012345678901\ 

20523456789012345678901234567890123456789012345678901234567890123456789012345678\ 

20690123456789012345678901234567890123456789012345678901234567890123456789012345\ 

20767890123456789012345678901234567890123456789012345678901234567890123456789012\ 

2083456789012345678901234567890123456789012345678901234567890123456789012345678\ 

20990123456789012345678901234567890123456789012345678901234567890123456789012345\ 

210678901234567890123456789012345678901234567890 

211 >>> try_int_div(large * 7, 1 * 7) 

212 123456789012345678901234567890123456789012345678901234567\ 

21389012345678901234567890123456789012345678901234567890123456789012345678901234\ 

21456789012345678901234567890123456789012345678901234567890123456789012345678901\ 

21523456789012345678901234567890123456789012345678901234567890123456789012345678\ 

21690123456789012345678901234567890123456789012345678901234567890123456789012345\ 

21767890123456789012345678901234567890123456789012345678901234567890123456789012\ 

2183456789012345678901234567890123456789012345678901234567890123456789012345678\ 

21990123456789012345678901234567890123456789012345678901234567890123456789012345\ 

220678901234567890123456789012345678901234567890 

221 >>> try: 

222 ... try_int_div(large, 7) 

223 ... except OverflowError as oe: 

224 ... print(oe) 

225 integer division result too large for a float 

226 >>> try: 

227 ... try_int_div(0, 0) 

228 ... except ZeroDivisionError as zde: 

229 ... print(zde) 

230 integer division or modulo by zero 

231 >>> try: 

232 ... try_int_div(1, 0) 

233 ... except ZeroDivisionError as zde: 

234 ... print(zde) 

235 integer division or modulo by zero 

236 >>> try: 

237 ... try_int_div(-1, 0) 

238 ... except ZeroDivisionError as zde: 

239 ... print(zde) 

240 integer division or modulo by zero 

241 """ 

242 # First try pure integer division, in case `a` is divisible by `b`. 

243 int_div_test: Final[int] = a // b 

244 if (int_div_test * b) == a: 

245 return int_div_test 

246 # If that does not work, use the floating point division. 

247 return __try_int(a / b) 

248 

249 

250def try_float_div(a: int | float, b: int | float) -> int | float: 

251 """ 

252 Try to divide two numbers at best precision. 

253 

254 First, we will check if we can convert the numbers to integers 

255 without loss of precision via :func:`try_int`. If yes, then 

256 we go for the maximum-precision integer division via :func:`try_int_div`. 

257 If no, then we do the normal floating point division and try to convert 

258 the result to an integer if that can be done without loss of precision. 

259 

260 :param a: the first number 

261 :param b: the second number 

262 :return: `a/b`, but always finite 

263 

264 :raises ValueError: if either one of the arguments or the final result 

265 would not be finite 

266 

267 >>> try_float_div(1e180, 1e60) 

268 1.0000000000000001e+120 

269 >>> try_float_div(1e60, 1e-60) 

270 1e+120 

271 >>> try_float_div(1e14, 1e-1) 

272 1000000000000000 

273 >>> try_float_div(1e14, -1e-1) 

274 -1000000000000000 

275 >>> try_float_div(-1e14, 1e-1) 

276 -1000000000000000 

277 >>> try_float_div(-1e14, -1e-1) 

278 1000000000000000 

279 >>> try_float_div(1e15, 1e-1) 

280 1e+16 

281 >>> try_float_div(1e15, -1e-1) 

282 -1e+16 

283 >>> try_float_div(-1e15, 1e-1) 

284 -1e+16 

285 >>> try_float_div(-1e15, -1e-1) 

286 1e+16 

287 >>> try_float_div(1e15, 1e-15) 

288 9.999999999999999e+29 

289 

290 >>> print(type(try_float_div(10, 2))) 

291 <class 'int'> 

292 >>> print(type(try_float_div(10, 3))) 

293 <class 'float'> 

294 >>> print(type(try_float_div(10, 0.5))) 

295 <class 'int'> 

296 

297 >>> from math import inf, nan 

298 >>> try: 

299 ... try_float_div(1.0, 0) 

300 ... except ZeroDivisionError as zde: 

301 ... print(zde) 

302 integer division or modulo by zero 

303 

304 >>> try: 

305 ... try_float_div(1.0, -0.0) 

306 ... except ZeroDivisionError as zde: 

307 ... print(zde) 

308 integer division or modulo by zero 

309 

310 >>> try: 

311 ... try_float_div(inf, 0) 

312 ... except ValueError as ve: 

313 ... print(ve) 

314 Value must be finite, but is inf. 

315 

316 >>> try: 

317 ... try_float_div(-inf, 0) 

318 ... except ValueError as ve: 

319 ... print(ve) 

320 Value must be finite, but is -inf. 

321 

322 >>> try: 

323 ... try_float_div(nan, 0) 

324 ... except ValueError as ve: 

325 ... print(ve) 

326 Value must be finite, but is nan. 

327 

328 >>> try: 

329 ... try_float_div(1, inf) 

330 ... except ValueError as ve: 

331 ... print(ve) 

332 Value must be finite, but is inf. 

333 

334 >>> try: 

335 ... try_float_div(1, -inf) 

336 ... except ValueError as ve: 

337 ... print(ve) 

338 Value must be finite, but is -inf. 

339 

340 >>> try: 

341 ... try_float_div(1, nan) 

342 ... except ValueError as ve: 

343 ... print(ve) 

344 Value must be finite, but is nan. 

345 

346 >>> try: 

347 ... try_float_div(1e300, 1e-60) 

348 ... except ValueError as ve: 

349 ... print(ve) 

350 Result must be finite, but is 1e+300/1e-60=inf. 

351 """ 

352 ia: Final[int | float] = try_int(a) 

353 ib: Final[int | float] = try_int(b) 

354 if isinstance(ia, int) and isinstance(ib, int): 

355 return try_int_div(ia, ib) 

356 val: Final[float] = ia / ib 

357 if not isfinite(val): 

358 raise ValueError(f"Result must be finite, but is {a}/{b}={val}.") 

359 return __try_int(val) 

360 

361 

362def __bin_search_root(value: int, power: int) -> int: 

363 """ 

364 Search a truncated integer root via binary search. 

365 

366 :param value: the integer value to compute the root of 

367 :param power: the integer power of the root 

368 :returns: the integer root, truncated if necessary 

369 """ 

370 int_sqrt: Final[int] = isqrt(value) 

371 if power == 2: 

372 return int_sqrt 

373 

374 # compute the maximum base root 

375 root_min: int = 1 

376 root_max: int = (int_sqrt - 1) if value > 3 else 1 

377 while root_max >= root_min: 

378 root_mid = isqrt(root_min * root_max) 

379 power_mid = root_mid ** power 

380 if power_mid > value: 

381 root_max = root_mid - 1 

382 elif power_mid < value: 

383 root_min = root_mid + 1 

384 else: 

385 return root_mid 

386 return root_max 

387 

388 

389def try_int_root(value: int, power: int, 

390 none_on_overflow: bool = True) -> int | float | None: 

391 """ 

392 Compute `value**(1/power)` where `value` and `power` are both integers. 

393 

394 :param value: the integer value to compute the root of 

395 :param power: the integer power of the root 

396 :param none_on_overflow: `True` if `None` should be returned if we 

397 encounter an :class:`OverflowError`, `False` if we should simply 

398 re-raise it 

399 :returns: the root, or `None` if we encounter an overflow 

400 

401 >>> try_int_root(100, 2) 

402 10 

403 >>> try_int_root(100, 3) - (100 ** (1/3)) 

404 0.0 

405 >>> try_int_root(123100, 23) - (123100 ** (1/23)) 

406 0.0 

407 >>> 123100**1000 - try_int_root(123100**1000, 1000)**1000 

408 0 

409 >>> 11**290 - try_int_root(11**290, 290)**290 

410 0 

411 >>> (abs(1.797E308 - try_int_root(int(1.797E308), 10) ** 10) 

412 ... < abs(1.797E308 - (int(1.797E308) ** 0.1) ** 10)) 

413 True 

414 """ 

415 if not isinstance(value, int): 

416 raise type_error(value, "value", int) 

417 if not isinstance(power, int): 

418 raise type_error(power, "power", int) 

419 if power <= 0: 

420 raise ValueError(f"power must be positive but is {power}.") 

421 if value <= 1: 

422 if value < 0: 

423 raise ValueError(f"value must not be negative, but is {value}.") 

424 return value 

425 if power == 1: 

426 return value 

427 

428 # first, approximate root with crude binary search 

429 int_root: int = __bin_search_root(value, power) 

430 root_power: int = int_root ** power 

431 if root_power >= value: 

432 if root_power == value: 

433 return int_root # ok, we are done 

434 raise ValueError(f"{int_root}**{power}={root_power} > {value}?") 

435 

436 # Now try to remove integer factors from the value and the root, 

437 # to achieve a more accurate division of the rest. 

438 # This is probably extremely inefficient. 

439 root_base: int = 1 

440 i: int = 2 

441 end: int = min(10_000, int_root) 

442 while i < end: 

443 div: int = i ** power 

444 if (value % div) == 0: 

445 root_base *= i 

446 value //= div 

447 int_root = (int_root + i - 1) // i 

448 end = min(end, int_root) 

449 else: 

450 i += 1 

451 

452 # value is now reduced by any integer factor that we can pull out 

453 # of the root. These factors are aggregated in root_base. 

454 # If root_base != 1, we need to re-compute the root of the remainder 

455 # inside value. Whatever root we now compute of value, we must multiply 

456 # it with root_base. 

457 if root_base != 1: 

458 int_root = __bin_search_root(value, power) 

459 

460 # check again 

461 root_power = int_root ** power 

462 if root_power >= value: 

463 if root_power == value: 

464 return root_base * int_root # ok, we are done 

465 raise ValueError(f"{int_root}**{power}={root_power} > {value}?") 

466 

467 # from now on, root may be either and int or (more likely) a float. 

468 root: int | float = int_root 

469 try: 

470 rest: int | float = try_int_div(value, root_power) 

471 rest_root: int | float = __try_int(rest ** (1.0 / power)) 

472 root = __try_int(root * rest_root) 

473 except OverflowError: # as ofe: 

474 if none_on_overflow: 

475 return None 

476 raise # raises ofe again 

477 

478 # OK, we got an approximate root of what remains of value. 

479 # Let's see if we can refine it. 

480 with contextlib.suppress(OverflowError): 

481 diff = abs((root ** power) - value) 

482 root2: int | float = __try_int(exp(log(value) / root)) 

483 diff2: int | float = abs((root2 ** power) - value) 

484 if diff2 < diff: 

485 diff = diff2 

486 root = root2 

487 root2 = __try_int(2 ** (log2(value) / root)) 

488 diff2 = abs((root2 ** power) - value) 

489 if diff2 < diff: 

490 diff = diff2 

491 root = root2 

492 

493 rdn = root 

494 rup = root 

495 while True: 

496 rdn = nextafter(rdn, -inf) 

497 apd = abs((rdn ** power) - value) 

498 if apd > diff: 

499 break 

500 diff = apd 

501 root = rdn 

502 while True: 

503 rup = nextafter(rup, inf) 

504 apd = abs((rup ** power) - value) 

505 if apd > diff: 

506 break 

507 diff = apd 

508 root = rup 

509 

510 return root_base * __try_int(root)