flexfloat 0.1.1__py3-none-any.whl → 0.1.3__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
flexfloat/core.py CHANGED
@@ -1,728 +1,1014 @@
1
- """Core FlexFloat class implementation."""
2
-
3
- from __future__ import annotations
4
-
5
- import math
6
- from typing import Final
7
-
8
- from .bitarray import BitArray
9
- from .types import Number
10
-
11
- LOG10_2: Final[float] = math.log10(2)
12
-
13
-
14
- class FlexFloat:
15
- """A class to represent a floating-point number with growable exponent and
16
- fixed-size fraction. This class is designed to handle very large or very
17
- small numbers by adjusting the exponent dynamically. While keeping the
18
- mantissa (fraction) fixed in size.
19
-
20
- This class follows the IEEE 754 double-precision floating-point format,
21
- but extends it to allow for a growable exponent and a fixed-size fraction.
22
-
23
- Attributes:
24
- sign (bool): The sign of the number (True for negative, False for positive).
25
- exponent (BitArray): A growable bit array representing the exponent
26
- (uses off-set binary representation).
27
- fraction (BitArray): A fixed-size bit array representing the fraction
28
- (mantissa) of the number.
29
- """
30
-
31
- def __init__(
32
- self,
33
- sign: bool = False,
34
- exponent: BitArray | None = None,
35
- fraction: BitArray | None = None,
36
- ):
37
- """Initialize a FlexFloat instance.
38
-
39
- Args:
40
- sign (bool): The sign of the number (True for negative, False for positive).
41
- exponent (BitArray | None): The exponent bit array (If None, represents 0).
42
- fraction (BitArray | None): The fraction bit array (If None, represents 0).
43
- """
44
- self.sign = sign
45
- self.exponent = exponent if exponent is not None else BitArray.zeros(11)
46
- self.fraction = fraction if fraction is not None else BitArray.zeros(52)
47
-
48
- @classmethod
49
- def from_float(cls, value: Number) -> FlexFloat:
50
- """Create a FlexFloat instance from a number.
51
-
52
- Args:
53
- value (Number): The number to convert to FlexFloat.
54
- Returns:
55
- FlexFloat: A new FlexFloat instance representing the number.
56
- """
57
- value = float(value)
58
- bits = BitArray.from_float(value)
59
-
60
- return cls(sign=bits[0], exponent=bits[1:12], fraction=bits[12:64])
61
-
62
- def to_float(self) -> float:
63
- """Convert the FlexFloat instance back to a 64-bit float.
64
-
65
- If float is bigger than 64 bits, it will truncate the value to fit.
66
-
67
- Returns:
68
- float: The floating-point number represented by the FlexFloat instance.
69
- Raises:
70
- ValueError: If the exponent or fraction lengths are not as expected.
71
- """
72
- if len(self.exponent) < 11 or len(self.fraction) < 52:
73
- raise ValueError("Must be a standard 64-bit FlexFloat")
74
-
75
- bits = BitArray([self.sign]) + self.exponent[:11] + self.fraction[:52]
76
- return bits.to_float()
77
-
78
- def __repr__(self) -> str:
79
- """Return a string representation of the FlexFloat instance.
80
-
81
- Returns:
82
- str: A string representation of the FlexFloat instance.
83
- """
84
- return (
85
- "FlexFloat("
86
- f"sign={self.sign}, "
87
- f"exponent={self.exponent}, "
88
- f"fraction={self.fraction})"
89
- )
90
-
91
- def pretty(self) -> str:
92
- """Return an easier to read string representation of the FlexFloat instance.
93
- Mainly converts the exponent and fraction to integers for readability.
94
-
95
- Returns:
96
- str: A pretty string representation of the FlexFloat instance.
97
- """
98
- sign = "-" if self.sign else ""
99
- exponent_value = self.exponent.to_signed_int() + 1
100
- fraction_value = self.fraction.to_int()
101
- return f"{sign}FlexFloat(exponent={exponent_value}, fraction={fraction_value})"
102
-
103
- @classmethod
104
- def nan(cls) -> FlexFloat:
105
- """Create a FlexFloat instance representing NaN (Not a Number).
106
-
107
- Returns:
108
- FlexFloat: A new FlexFloat instance representing NaN.
109
- """
110
- exponent = BitArray.ones(11)
111
- fraction = BitArray.ones(52)
112
- return cls(sign=True, exponent=exponent, fraction=fraction)
113
-
114
- @classmethod
115
- def infinity(cls, sign: bool = False) -> FlexFloat:
116
- """Create a FlexFloat instance representing Infinity.
117
-
118
- Args:
119
- sign (bool): Indicates if the infinity is negative.
120
- Returns:
121
- FlexFloat: A new FlexFloat instance representing Infinity.
122
- """
123
- exponent = BitArray.ones(11)
124
- fraction = BitArray.zeros(52)
125
- return cls(sign=sign, exponent=exponent, fraction=fraction)
126
-
127
- @classmethod
128
- def zero(cls) -> FlexFloat:
129
- """Create a FlexFloat instance representing zero.
130
-
131
- Returns:
132
- FlexFloat: A new FlexFloat instance representing zero.
133
- """
134
- exponent = BitArray.zeros(11)
135
- fraction = BitArray.zeros(52)
136
- return cls(sign=False, exponent=exponent, fraction=fraction)
137
-
138
- def _is_special_exponent(self) -> bool:
139
- """Check if the exponent represents a special value (NaN or Infinity).
140
-
141
- Returns:
142
- bool: True if the exponent is at its maximum value, False otherwise.
143
- """
144
- # In IEEE 754, special values have all exponent bits set to 1
145
- # This corresponds to the maximum value in the unsigned representation
146
- # For signed offset binary, the maximum value is 2^(n-1) - 1
147
- # where n is the number of bits
148
- max_signed_value = (1 << (len(self.exponent) - 1)) - 1
149
- return self.exponent.to_signed_int() == max_signed_value
150
-
151
- def is_nan(self) -> bool:
152
- """Check if the FlexFloat instance represents NaN (Not a Number).
153
-
154
- Returns:
155
- bool: True if the FlexFloat instance is NaN, False otherwise.
156
- """
157
- return self._is_special_exponent() and any(self.fraction)
158
-
159
- def is_infinity(self) -> bool:
160
- """Check if the FlexFloat instance represents Infinity.
161
-
162
- Returns:
163
- bool: True if the FlexFloat instance is Infinity, False otherwise.
164
- """
165
- return self._is_special_exponent() and not any(self.fraction)
166
-
167
- def is_zero(self) -> bool:
168
- """Check if the FlexFloat instance represents zero.
169
-
170
- Returns:
171
- bool: True if the FlexFloat instance is zero, False otherwise.
172
- """
173
- return not any(self.exponent) and not any(self.fraction)
174
-
175
- def copy(self) -> FlexFloat:
176
- """Create a copy of the FlexFloat instance.
177
-
178
- Returns:
179
- FlexFloat: A new FlexFloat instance with the same data as the original.
180
- """
181
- return FlexFloat(
182
- sign=self.sign, exponent=self.exponent.copy(), fraction=self.fraction.copy()
183
- )
184
-
185
- def __str__(self) -> str:
186
- """Float representation of the FlexFloat using a generic algorithm.
187
-
188
- This implementation doesn't rely on Python's float conversion and instead
189
- implements the formatting logic directly, making it work for any exponent size.
190
-
191
- Currently, it only operates in scientific notation with 5 decimal places.
192
- """
193
- sign_str = "-" if self.sign else ""
194
- # Handle special cases first
195
- if self.is_nan():
196
- return "nan"
197
-
198
- if self.is_infinity():
199
- return f"{sign_str}inf"
200
-
201
- if self.is_zero():
202
- return f"{sign_str}0.00000e+00"
203
-
204
- exponent = self.exponent.to_signed_int() + 1
205
-
206
- # Convert fraction to decimal value between 1 and 2
207
- # (starting with 1.0 for the implicit leading bit)
208
- mantissa = 1.0
209
- for i, bit in enumerate(self.fraction):
210
- if bit:
211
- mantissa += 1.0 / (1 << (i + 1))
212
-
213
- # To avoid overflow with very large exponents, work in log space
214
- # log10(mantissa * 2^exponent) = log10(mantissa) + exponent * log10(2)
215
- log10_mantissa = math.log10(mantissa)
216
- log10_total = log10_mantissa + exponent * LOG10_2
217
-
218
- decimal_exponent = int(log10_total)
219
-
220
- log10_normalized = log10_total - decimal_exponent
221
- normalized_mantissa = math.pow(10, log10_normalized)
222
-
223
- # Ensure the mantissa is properly normalized (between 1.0 and 10.0)
224
- while normalized_mantissa >= 10.0:
225
- normalized_mantissa /= 10.0
226
- decimal_exponent += 1
227
- while normalized_mantissa < 1.0:
228
- normalized_mantissa *= 10.0
229
- decimal_exponent -= 1
230
-
231
- # Format with 5 decimal places
232
- return f"{sign_str}{normalized_mantissa:.5f}e{decimal_exponent:+03d}"
233
-
234
- def __neg__(self) -> FlexFloat:
235
- """Negate the FlexFloat instance."""
236
- return FlexFloat(
237
- sign=not self.sign,
238
- exponent=self.exponent.copy(),
239
- fraction=self.fraction.copy(),
240
- )
241
-
242
- @staticmethod
243
- def _grow_exponent(exponent: int, exponent_length: int) -> int:
244
- """Grow the exponent if it exceeds the maximum value for the current length.
245
-
246
- Args:
247
- exponent (int): The current exponent value.
248
- exponent_length (int): The current length of the exponent in bits.
249
- Returns:
250
- int: The new exponent length if it needs to be grown, otherwise the same
251
- length.
252
- """
253
- while True:
254
- half = 1 << (exponent_length - 1)
255
- min_exponent = -half
256
- max_exponent = half - 1
257
-
258
- if min_exponent <= exponent <= max_exponent:
259
- break
260
- exponent_length += 1
261
-
262
- return exponent_length
263
-
264
- def __add__(self, other: FlexFloat | Number) -> FlexFloat:
265
- """Add two FlexFloat instances together.
266
-
267
- Args:
268
- other (FlexFloat | float | int): The other FlexFloat instance to add.
269
- Returns:
270
- FlexFloat: A new FlexFloat instance representing the sum.
271
- """
272
- if isinstance(other, Number):
273
- other = FlexFloat.from_float(other)
274
- if not isinstance(other, FlexFloat):
275
- raise TypeError("Can only add FlexFloat instances.")
276
-
277
- if self.sign != other.sign:
278
- return self - (-other)
279
-
280
- # OBJECTIVE: Add two FlexFloat instances together.
281
- # https://www.sciencedirect.com/topics/computer-science/floating-point-addition
282
- # and: https://cse.hkust.edu.hk/~cktang/cs180/notes/lec21.pdf
283
- #
284
- # Steps:
285
- # 0. Handle special cases (NaN, Infinity).
286
- # 1. Extract exponent and fraction bits.
287
- # 2. Prepend leading 1 to form the mantissa.
288
- # 3. Compare exponents.
289
- # 4. Shift smaller mantissa if necessary.
290
- # 5. Add mantissas.
291
- # 6. Normalize mantissa and adjust exponent if necessary.
292
- # 7. Grow exponent if necessary.
293
- # 8. Round result.
294
- # 9. Return new FlexFloat instance.
295
-
296
- # Step 0: Handle special cases
297
- if self.is_zero() or other.is_zero():
298
- return self.copy() if other.is_zero() else other.copy()
299
-
300
- if self.is_nan() or other.is_nan():
301
- return FlexFloat.nan()
302
-
303
- if self.is_infinity() and other.is_infinity():
304
- return self.copy() if self.sign == other.sign else FlexFloat.nan()
305
- if self.is_infinity() or other.is_infinity():
306
- return self.copy() if self.is_infinity() else other.copy()
307
-
308
- # Step 1: Extract exponent and fraction bits
309
- exponent_self = self.exponent.to_signed_int() + 1
310
- exponent_other = other.exponent.to_signed_int() + 1
311
-
312
- # Step 2: Prepend leading 1 to form the mantissa
313
- mantissa_self = [True] + self.fraction
314
- mantissa_other = [True] + other.fraction
315
-
316
- # Step 3: Compare exponents (self is always larger or equal)
317
- if exponent_self < exponent_other:
318
- exponent_self, exponent_other = exponent_other, exponent_self
319
- mantissa_self, mantissa_other = mantissa_other, mantissa_self
320
-
321
- # Step 4: Shift smaller mantissa if necessary
322
- if exponent_self > exponent_other:
323
- shift_amount = exponent_self - exponent_other
324
- mantissa_other = mantissa_other.shift(shift_amount)
325
-
326
- # Step 5: Add mantissas
327
- assert (
328
- len(mantissa_self) == 53
329
- ), "Fraction must be 53 bits long. (1 leading bit + 52 fraction bits)"
330
- assert len(mantissa_self) == len(mantissa_other), (
331
- f"Mantissas must be the same length. Expected 53 bits, "
332
- f"got {len(mantissa_other)} bits."
333
- )
334
-
335
- mantissa_result = BitArray.zeros(53) # 1 leading bit + 52 fraction bits
336
- carry = False
337
- for i in range(52, -1, -1):
338
- total = mantissa_self[i] + mantissa_other[i] + carry
339
- mantissa_result[i] = total % 2 == 1
340
- carry = total > 1
341
-
342
- # Step 6: Normalize mantissa and adjust exponent if necessary
343
- # Only need to normalize if there is a carry
344
- if carry:
345
- # Insert the carry bit and shift right
346
- mantissa_result = mantissa_result.shift(1, fill=True)
347
- exponent_self += 1
348
-
349
- # Step 7: Grow exponent if necessary
350
- exp_result_length = self._grow_exponent(exponent_self, len(self.exponent))
351
- assert (
352
- exponent_self - (1 << (exp_result_length - 1)) < 2
353
- ), "Exponent growth should not exceed 1 bit."
354
-
355
- exponent_result = BitArray.from_signed_int(exponent_self - 1, exp_result_length)
356
- return FlexFloat(
357
- sign=self.sign,
358
- exponent=exponent_result,
359
- fraction=mantissa_result[1:], # Exclude leading bit
360
- )
361
-
362
- def __sub__(self, other: FlexFloat | Number) -> FlexFloat:
363
- """Subtract one FlexFloat instance from another.
364
-
365
- Args:
366
- other (FlexFloat | float | int): The FlexFloat instance to subtract.
367
- Returns:
368
- FlexFloat: A new FlexFloat instance representing the difference.
369
- """
370
- if isinstance(other, Number):
371
- other = FlexFloat.from_float(other)
372
- if not isinstance(other, FlexFloat):
373
- raise TypeError("Can only subtract FlexFloat instances.")
374
-
375
- # If signs are different, subtraction becomes addition
376
- if self.sign != other.sign:
377
- return self + (-other)
378
-
379
- # OBJECTIVE: Subtract two FlexFloat instances.
380
- # Based on floating-point subtraction algorithms
381
- #
382
- # Steps:
383
- # 0. Handle special cases (NaN, Infinity, zero).
384
- # 1. Extract exponent and fraction bits.
385
- # 2. Prepend leading 1 to form the mantissa.
386
- # 3. Compare exponents and align mantissas.
387
- # 4. Compare magnitudes to determine result sign.
388
- # 5. Subtract mantissas (larger - smaller).
389
- # 6. Normalize mantissa and adjust exponent if necessary.
390
- # 7. Grow exponent if necessary.
391
- # 8. Return new FlexFloat instance.
392
-
393
- # Step 0: Handle special cases
394
- if self.is_zero() or other.is_zero():
395
- return self.copy() if other.is_zero() else -other.copy()
396
-
397
- if self.is_nan() or other.is_nan():
398
- return FlexFloat.nan()
399
-
400
- if self.is_infinity() and other.is_infinity():
401
- if self.sign == other.sign:
402
- return FlexFloat.nan() # inf - inf = NaN
403
- return self.copy() # inf - (-inf) = inf
404
-
405
- if self.is_infinity():
406
- return self.copy()
407
-
408
- if other.is_infinity():
409
- return -other
410
-
411
- # Step 1: Extract exponent and fraction bits
412
- exponent_self = self.exponent.to_signed_int() + 1
413
- exponent_other = other.exponent.to_signed_int() + 1
414
-
415
- # Step 2: Prepend leading 1 to form the mantissa
416
- mantissa_self = [True] + self.fraction
417
- mantissa_other = [True] + other.fraction
418
-
419
- # Step 3: Align mantissas by shifting the smaller exponent
420
- result_sign = self.sign
421
- shift_amount = abs(exponent_self - exponent_other)
422
- if exponent_self >= exponent_other:
423
- mantissa_other = mantissa_other.shift(shift_amount)
424
- result_exponent = exponent_self
425
- else:
426
- mantissa_self = mantissa_self.shift(shift_amount)
427
- result_exponent = exponent_other
428
-
429
- # Step 4: Compare magnitudes to determine which mantissa is larger
430
- # Convert mantissas to integers for comparison
431
- mantissa_self_int = mantissa_self.to_int()
432
- mantissa_other_int = mantissa_other.to_int()
433
-
434
- if mantissa_self_int >= mantissa_other_int:
435
- larger_mantissa = mantissa_self
436
- smaller_mantissa = mantissa_other
437
- result_sign = self.sign
438
- else:
439
- larger_mantissa = mantissa_other
440
- smaller_mantissa = mantissa_self
441
- # Flip sign since we're computing -(smaller - larger)
442
- result_sign = not self.sign
443
-
444
- # Step 5: Subtract mantissas (larger - smaller)
445
- assert (
446
- len(larger_mantissa) == 53
447
- ), "Mantissa must be 53 bits long. (1 leading bit + 52 fraction bits)"
448
- assert len(larger_mantissa) == len(smaller_mantissa), (
449
- f"Mantissas must be the same length. Expected 53 bits, "
450
- f"got {len(smaller_mantissa)} bits."
451
- )
452
-
453
- mantissa_result = BitArray.zeros(53)
454
- borrow = False
455
- for i in range(52, -1, -1):
456
- diff = int(larger_mantissa[i]) - int(smaller_mantissa[i]) - int(borrow)
457
-
458
- mantissa_result[i] = diff % 2 == 1
459
- borrow = diff < 0
460
-
461
- assert not borrow, "Subtraction should not result in a negative mantissa."
462
-
463
- # Step 6: Normalize mantissa and adjust exponent if necessary
464
- # Find the first 1 bit (leading bit might have been canceled out)
465
- leading_zero_count = next(
466
- (i for i, bit in enumerate(mantissa_result) if bit), len(mantissa_result)
467
- )
468
-
469
- # Handle case where result becomes zero or denormalized
470
- if leading_zero_count >= 53:
471
- return FlexFloat.from_float(0.0)
472
-
473
- if leading_zero_count > 0:
474
- # Shift left to normalize
475
- mantissa_result = mantissa_result.shift(-leading_zero_count)
476
- result_exponent -= leading_zero_count
477
-
478
- # Step 7: Grow exponent if necessary (handle underflow)
479
- exp_result_length = self._grow_exponent(result_exponent, len(self.exponent))
480
-
481
- exp_result = BitArray.from_signed_int(result_exponent - 1, exp_result_length)
482
-
483
- return FlexFloat(
484
- sign=result_sign,
485
- exponent=exp_result,
486
- fraction=mantissa_result[1:], # Exclude leading bit
487
- )
488
-
489
- def __mul__(self, other: FlexFloat | Number) -> FlexFloat:
490
- """Multiply two FlexFloat instances together.
491
-
492
- Args:
493
- other (FlexFloat | float | int): The other FlexFloat instance to multiply.
494
- Returns:
495
- FlexFloat: A new FlexFloat instance representing the product.
496
- """
497
- if isinstance(other, Number):
498
- other = FlexFloat.from_float(other)
499
- if not isinstance(other, FlexFloat):
500
- raise TypeError("Can only multiply FlexFloat instances.")
501
-
502
- # OBJECTIVE: Multiply two FlexFloat instances together.
503
- # https://www.rfwireless-world.com/tutorials/ieee-754-floating-point-arithmetic
504
- #
505
- # Steps:
506
- # 0. Handle special cases (NaN, Infinity, zero).
507
- # 1. Calculate result sign (XOR of operand signs).
508
- # 2. Extract and add exponents (subtract bias).
509
- # 3. Multiply mantissas.
510
- # 4. Normalize mantissa and adjust exponent if necessary.
511
- # 5. Check for overflow/underflow.
512
- # 6. Grow exponent if necessary.
513
- # 7. Return new FlexFloat instance.
514
-
515
- # Step 0: Handle special cases
516
- if self.is_nan() or other.is_nan():
517
- return FlexFloat.nan()
518
-
519
- if self.is_zero() or other.is_zero():
520
- return FlexFloat.zero()
521
-
522
- if self.is_infinity() or other.is_infinity():
523
- result_sign = self.sign ^ other.sign
524
- return FlexFloat.infinity(sign=result_sign)
525
-
526
- # Step 1: Calculate result sign (XOR of signs)
527
- result_sign = self.sign ^ other.sign
528
-
529
- # Step 2: Extract exponent and fraction bits
530
- # Note: The stored exponent needs +1 to get the actual value (like in addition)
531
- exponent_self = self.exponent.to_signed_int() + 1
532
- exponent_other = other.exponent.to_signed_int() + 1
533
-
534
- # Step 3: Add exponents
535
- # When multiplying, we add the unbiased exponents
536
- result_exponent = exponent_self + exponent_other
537
-
538
- # Step 4: Multiply mantissas
539
- # Prepend leading 1 to form the mantissa (1.fraction)
540
- mantissa_self = [True] + self.fraction
541
- mantissa_other = [True] + other.fraction
542
-
543
- # Convert mantissas to integers for multiplication
544
- mantissa_self_int = mantissa_self.to_int()
545
- mantissa_other_int = mantissa_other.to_int()
546
-
547
- # Multiply the mantissas
548
- product = mantissa_self_int * mantissa_other_int
549
-
550
- # Convert back to bit array
551
- # The product will have up to 106 bits (53 + 53)
552
- if product == 0:
553
- return FlexFloat.zero()
554
-
555
- product_bits = BitArray.zeros(106)
556
- for i in range(105, -1, -1):
557
- product_bits[i] = product & 1 == 1
558
- product >>= 1
559
- if product <= 0:
560
- break
561
-
562
- # Step 5: Normalize mantissa and adjust exponent if necessary
563
- # Find the position of the most significant bit
564
- msb_position = next((i for i, bit in enumerate(product_bits) if bit), None)
565
-
566
- assert msb_position is not None, "Product should not be zero here."
567
-
568
- # The mantissa multiplication gives us a result with 2 integer bits
569
- # We need to normalize to have exactly 1 integer bit
570
- # If MSB is at position 0, we have a 2-bit integer part (11.xxxxx)
571
- # If MSB is at position 1, we have a 1-bit integer part (1.xxxxx)
572
- if msb_position == 0:
573
- result_exponent += 1
574
- normalized_mantissa = product_bits[msb_position : msb_position + 53]
575
-
576
- # Pad with zeros if we don't have enough bits
577
- missing_bits = 53 - len(normalized_mantissa)
578
- if missing_bits > 0:
579
- normalized_mantissa += [False] * missing_bits
580
-
581
- # Step 6: Grow exponent if necessary to accommodate the result
582
- exp_result_length = max(len(self.exponent), len(other.exponent))
583
-
584
- # Check if we need to grow the exponent to accommodate the result
585
- exp_result_length = self._grow_exponent(result_exponent, exp_result_length)
586
-
587
- exp_result = BitArray.from_signed_int(result_exponent - 1, exp_result_length)
588
-
589
- return FlexFloat(
590
- sign=result_sign,
591
- exponent=exp_result,
592
- fraction=normalized_mantissa[1:], # Exclude leading bit
593
- )
594
-
595
- def __rmul__(self, other: Number) -> FlexFloat:
596
- """Right-hand multiplication for Number types.
597
-
598
- Args:
599
- other (float | int): The number to multiply with this FlexFloat.
600
- Returns:
601
- FlexFloat: A new FlexFloat instance representing the product.
602
- """
603
- return self * other
604
-
605
- def __truediv__(self, other: FlexFloat | Number) -> FlexFloat:
606
- """Divide this FlexFloat by another FlexFloat or number.
607
-
608
- Args:
609
- other (FlexFloat | float | int): The divisor.
610
- Returns:
611
- FlexFloat: A new FlexFloat instance representing the quotient.
612
- """
613
- if isinstance(other, Number):
614
- other = FlexFloat.from_float(other)
615
- if not isinstance(other, FlexFloat):
616
- raise TypeError("Can only divide FlexFloat instances.")
617
-
618
- # OBJECTIVE: Divide two FlexFloat instances.
619
- # https://www.rfwireless-world.com/tutorials/ieee-754-floating-point-arithmetic
620
- #
621
- # Steps:
622
- # 0. Handle special cases (NaN, Infinity, zero).
623
- # 1. Calculate result sign (XOR of operand signs).
624
- # 2. Extract and subtract exponents (add bias).
625
- # 3. Divide mantissas.
626
- # 4. Normalize mantissa and adjust exponent if necessary.
627
- # 5. Check for overflow/underflow.
628
- # 6. Grow exponent if necessary.
629
- # 7. Return new FlexFloat instance.
630
-
631
- # Step 0: Handle special cases
632
- if self.is_nan() or other.is_nan():
633
- return FlexFloat.nan()
634
-
635
- # Zero cases
636
- if self.is_zero() and other.is_zero():
637
- return FlexFloat.nan() # 0 / 0 = NaN
638
- if self.is_zero() and not other.is_zero():
639
- return FlexFloat.zero() # 0 / finite = 0
640
- if not self.is_zero() and other.is_zero():
641
- return FlexFloat.infinity(sign=self.sign ^ other.sign) # finite / 0 = inf
642
-
643
- # Infinity cases
644
- if self.is_infinity() and other.is_infinity():
645
- return FlexFloat.nan() # inf / inf = NaN
646
- if self.is_infinity():
647
- return FlexFloat.infinity(sign=self.sign ^ other.sign) # inf / finite = inf
648
- if other.is_infinity():
649
- return FlexFloat.zero() # finite / inf = 0
650
-
651
- # Step 1: Calculate result sign (XOR of signs)
652
- result_sign = self.sign ^ other.sign
653
-
654
- # Step 2: Extract exponent and fraction bits
655
- # Note: The stored exponent needs +1 to get the actual value
656
- # (like in multiplication)
657
- exponent_self = self.exponent.to_signed_int() + 1
658
- exponent_other = other.exponent.to_signed_int() + 1
659
-
660
- # Step 3: Subtract exponents (for division, we subtract the divisor's exponent)
661
- result_exponent = exponent_self - exponent_other
662
-
663
- # Step 4: Divide mantissas
664
- # Prepend leading 1 to form the mantissa (1.fraction)
665
- mantissa_self = [True] + self.fraction
666
- mantissa_other = [True] + other.fraction
667
-
668
- # Convert mantissas to integers for division
669
- mantissa_self_int = mantissa_self.to_int()
670
- mantissa_other_int = mantissa_other.to_int()
671
-
672
- # Normalize mantissa for division (avoid overflow) -> scale the dividend
673
- if mantissa_self_int < mantissa_other_int:
674
- scale_factor = 1 << 53
675
- result_exponent -= 1 # Adjust exponent since result < 1.0
676
- else:
677
- scale_factor = 1 << 52
678
- scaled_dividend = mantissa_self_int * scale_factor
679
- quotient = scaled_dividend // mantissa_other_int
680
-
681
- if quotient == 0:
682
- return FlexFloat.zero()
683
-
684
- # Convert quotient to BitArray for easier bit manipulation
685
- quotient_bitarray = BitArray.zeros(64) # Use a fixed size for consistency
686
- temp_quotient = quotient
687
- bit_pos = 63
688
- while temp_quotient > 0 and bit_pos >= 0:
689
- quotient_bitarray[bit_pos] = (temp_quotient & 1) == 1
690
- temp_quotient >>= 1
691
- bit_pos -= 1
692
-
693
- # Step 5: Normalize mantissa and adjust exponent if necessary
694
- # Find the position of the most significant bit (first 1)
695
- msb_pos = next((i for i, bit in enumerate(quotient_bitarray) if bit), None)
696
-
697
- if msb_pos is None:
698
- return FlexFloat.zero()
699
-
700
- # Extract exactly 53 bits starting from the MSB (1 integer + 52 fraction)
701
- normalized_mantissa = quotient_bitarray[msb_pos : msb_pos + 53]
702
- normalized_mantissa = normalized_mantissa.shift(
703
- 53 - len(normalized_mantissa), fill=False
704
- )
705
-
706
- # Step 6: Grow exponent if necessary to accommodate the result
707
- exp_result_length = max(len(self.exponent), len(other.exponent))
708
-
709
- # Check if we need to grow the exponent to accommodate the result
710
- exp_result_length = self._grow_exponent(result_exponent, exp_result_length)
711
-
712
- exp_result = BitArray.from_signed_int(result_exponent - 1, exp_result_length)
713
-
714
- return FlexFloat(
715
- sign=result_sign,
716
- exponent=exp_result,
717
- fraction=normalized_mantissa[1:], # Exclude leading bit
718
- )
719
-
720
- def __rtruediv__(self, other: Number) -> FlexFloat:
721
- """Right-hand division for Number types.
722
-
723
- Args:
724
- other (float | int): The number to divide by this FlexFloat.
725
- Returns:
726
- FlexFloat: A new FlexFloat instance representing the quotient.
727
- """
728
- return FlexFloat.from_float(other) / self
1
+ """Core FlexFloat class implementation."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import math
6
+ from typing import ClassVar, Final
7
+
8
+ from .bitarray import BitArray, BitArrayType
9
+ from .types import Number
10
+
11
+ LOG10_2: Final[float] = math.log10(2)
12
+
13
+
14
+ class FlexFloat:
15
+ """A class to represent a floating-point number with growable exponent and
16
+ fixed-size fraction. This class is designed to handle very large or very
17
+ small numbers by adjusting the exponent dynamically. While keeping the
18
+ mantissa (fraction) fixed in size.
19
+
20
+ This class follows the IEEE 754 double-precision floating-point format,
21
+ but extends it to allow for a growable exponent and a fixed-size fraction.
22
+
23
+ Attributes:
24
+ sign (bool): The sign of the number (True for negative, False for positive).
25
+ exponent (BitArray): A growable bit array representing the exponent
26
+ (uses off-set binary representation).
27
+ fraction (BitArray): A fixed-size bit array representing the fraction
28
+ (mantissa) of the number.
29
+ """
30
+
31
+ e: ClassVar[FlexFloat]
32
+
33
+ def __init__(
34
+ self,
35
+ sign: bool = False,
36
+ exponent: BitArray | None = None,
37
+ fraction: BitArray | None = None,
38
+ ):
39
+ """Initialize a FlexFloat instance.
40
+
41
+ Args:
42
+ sign (bool): The sign of the number (True for negative, False for positive).
43
+ exponent (BitArray | None): The exponent bit array (If None, represents 0).
44
+ fraction (BitArray | None): The fraction bit array (If None, represents 0).
45
+ """
46
+ self.sign = sign
47
+ self.exponent = exponent if exponent is not None else BitArrayType.zeros(11)
48
+ self.fraction = fraction if fraction is not None else BitArrayType.zeros(52)
49
+
50
+ @classmethod
51
+ def from_float(cls, value: Number) -> FlexFloat:
52
+ """Create a FlexFloat instance from a number.
53
+
54
+ Args:
55
+ value (Number): The number to convert to FlexFloat.
56
+ Returns:
57
+ FlexFloat: A new FlexFloat instance representing the number.
58
+ """
59
+ value = float(value)
60
+ bits = BitArrayType.from_float(value)
61
+
62
+ return cls(sign=bits[0], exponent=bits[1:12], fraction=bits[12:64])
63
+
64
+ def to_float(self) -> float:
65
+ """Convert the FlexFloat instance back to a 64-bit float.
66
+
67
+ If float is bigger than 64 bits, it will truncate the value to fit.
68
+
69
+ Returns:
70
+ float: The floating-point number represented by the FlexFloat instance.
71
+ Raises:
72
+ ValueError: If the exponent or fraction lengths are not as expected.
73
+ """
74
+ if len(self.exponent) < 11 or len(self.fraction) < 52:
75
+ raise ValueError("Must be a standard 64-bit FlexFloat")
76
+
77
+ bits = BitArrayType([self.sign]) + self.exponent[:11] + self.fraction[:52]
78
+ return bits.to_float()
79
+
80
+ def __repr__(self) -> str:
81
+ """Return a string representation of the FlexFloat instance.
82
+
83
+ Returns:
84
+ str: A string representation of the FlexFloat instance.
85
+ """
86
+ return (
87
+ "FlexFloat("
88
+ f"sign={self.sign}, "
89
+ f"exponent={self.exponent}, "
90
+ f"fraction={self.fraction})"
91
+ )
92
+
93
+ def pretty(self) -> str:
94
+ """Return an easier to read string representation of the FlexFloat instance.
95
+ Mainly converts the exponent and fraction to integers for readability.
96
+
97
+ Returns:
98
+ str: A pretty string representation of the FlexFloat instance.
99
+ """
100
+ sign = "-" if self.sign else ""
101
+ exponent_value = self.exponent.to_signed_int() + 1
102
+ fraction_value = self.fraction.to_int()
103
+ return f"{sign}FlexFloat(exponent={exponent_value}, fraction={fraction_value})"
104
+
105
+ @classmethod
106
+ def nan(cls) -> FlexFloat:
107
+ """Create a FlexFloat instance representing NaN (Not a Number).
108
+
109
+ Returns:
110
+ FlexFloat: A new FlexFloat instance representing NaN.
111
+ """
112
+ exponent = BitArrayType.ones(11)
113
+ fraction = BitArrayType.ones(52)
114
+ return cls(sign=True, exponent=exponent, fraction=fraction)
115
+
116
+ @classmethod
117
+ def infinity(cls, sign: bool = False) -> FlexFloat:
118
+ """Create a FlexFloat instance representing Infinity.
119
+
120
+ Args:
121
+ sign (bool): Indicates if the infinity is negative.
122
+ Returns:
123
+ FlexFloat: A new FlexFloat instance representing Infinity.
124
+ """
125
+ exponent = BitArrayType.ones(11)
126
+ fraction = BitArrayType.zeros(52)
127
+ return cls(sign=sign, exponent=exponent, fraction=fraction)
128
+
129
+ @classmethod
130
+ def zero(cls) -> FlexFloat:
131
+ """Create a FlexFloat instance representing zero.
132
+
133
+ Returns:
134
+ FlexFloat: A new FlexFloat instance representing zero.
135
+ """
136
+ exponent = BitArrayType.zeros(11)
137
+ fraction = BitArrayType.zeros(52)
138
+ return cls(sign=False, exponent=exponent, fraction=fraction)
139
+
140
+ def _is_special_exponent(self) -> bool:
141
+ """Check if the exponent represents a special value (NaN or Infinity).
142
+
143
+ Returns:
144
+ bool: True if the exponent is at its maximum value, False otherwise.
145
+ """
146
+ # In IEEE 754, special values have all exponent bits set to 1
147
+ # This corresponds to the maximum value in the unsigned representation
148
+ # For signed offset binary, the maximum value is 2^(n-1) - 1
149
+ # where n is the number of bits
150
+ max_signed_value = (1 << (len(self.exponent) - 1)) - 1
151
+ return self.exponent.to_signed_int() == max_signed_value
152
+
153
+ def is_nan(self) -> bool:
154
+ """Check if the FlexFloat instance represents NaN (Not a Number).
155
+
156
+ Returns:
157
+ bool: True if the FlexFloat instance is NaN, False otherwise.
158
+ """
159
+ return self._is_special_exponent() and any(self.fraction)
160
+
161
+ def is_infinity(self) -> bool:
162
+ """Check if the FlexFloat instance represents Infinity.
163
+
164
+ Returns:
165
+ bool: True if the FlexFloat instance is Infinity, False otherwise.
166
+ """
167
+ return self._is_special_exponent() and not any(self.fraction)
168
+
169
+ def is_zero(self) -> bool:
170
+ """Check if the FlexFloat instance represents zero.
171
+
172
+ Returns:
173
+ bool: True if the FlexFloat instance is zero, False otherwise.
174
+ """
175
+ return not any(self.exponent) and not any(self.fraction)
176
+
177
+ def copy(self) -> FlexFloat:
178
+ """Create a copy of the FlexFloat instance.
179
+
180
+ Returns:
181
+ FlexFloat: A new FlexFloat instance with the same data as the original.
182
+ """
183
+ return FlexFloat(
184
+ sign=self.sign, exponent=self.exponent.copy(), fraction=self.fraction.copy()
185
+ )
186
+
187
+ def __str__(self) -> str:
188
+ """Float representation of the FlexFloat using a generic algorithm.
189
+
190
+ This implementation doesn't rely on Python's float conversion and instead
191
+ implements the formatting logic directly, making it work for any exponent size.
192
+
193
+ Currently, it only operates in scientific notation with 5 decimal places.
194
+ """
195
+ sign_str = "-" if self.sign else ""
196
+ # Handle special cases first
197
+ if self.is_nan():
198
+ return "nan"
199
+
200
+ if self.is_infinity():
201
+ return f"{sign_str}inf"
202
+
203
+ if self.is_zero():
204
+ return f"{sign_str}0.00000e+00"
205
+
206
+ exponent = self.exponent.to_signed_int() + 1
207
+
208
+ # Convert fraction to decimal value between 1 and 2
209
+ # (starting with 1.0 for the implicit leading bit)
210
+ mantissa = 1.0
211
+ for i, bit in enumerate(self.fraction):
212
+ if bit:
213
+ mantissa += 1.0 / (1 << (i + 1))
214
+
215
+ # To avoid overflow with very large exponents, work in log space
216
+ # log10(mantissa * 2^exponent) = log10(mantissa) + exponent * log10(2)
217
+ log10_mantissa = math.log10(mantissa)
218
+ log10_total = log10_mantissa + exponent * LOG10_2
219
+
220
+ decimal_exponent = int(log10_total)
221
+
222
+ log10_normalized = log10_total - decimal_exponent
223
+ normalized_mantissa = math.pow(10, log10_normalized)
224
+
225
+ # Ensure the mantissa is properly normalized (between 1.0 and 10.0)
226
+ while normalized_mantissa >= 10.0:
227
+ normalized_mantissa /= 10.0
228
+ decimal_exponent += 1
229
+
230
+ while normalized_mantissa < 1.0:
231
+ normalized_mantissa *= 10.0
232
+ decimal_exponent -= 1
233
+
234
+ # Format with 5 decimal places
235
+ return f"{sign_str}{normalized_mantissa:.5f}e{decimal_exponent:+03d}"
236
+
237
+ def __neg__(self) -> FlexFloat:
238
+ """Negate the FlexFloat instance."""
239
+ return FlexFloat(
240
+ sign=not self.sign,
241
+ exponent=self.exponent.copy(),
242
+ fraction=self.fraction.copy(),
243
+ )
244
+
245
+ @staticmethod
246
+ def _grow_exponent(exponent: int, exponent_length: int) -> int:
247
+ """Grow the exponent if it exceeds the maximum value for the current length.
248
+
249
+ Args:
250
+ exponent (int): The current exponent value.
251
+ exponent_length (int): The current length of the exponent in bits.
252
+ Returns:
253
+ int: The new exponent length if it needs to be grown, otherwise the same
254
+ length.
255
+ """
256
+ while True:
257
+ half = 1 << (exponent_length - 1)
258
+ min_exponent = -half
259
+ max_exponent = half - 1
260
+
261
+ if min_exponent <= exponent <= max_exponent:
262
+ break
263
+ exponent_length += 1
264
+
265
+ return exponent_length
266
+
267
+ def __add__(self, other: FlexFloat | Number) -> FlexFloat:
268
+ """Add two FlexFloat instances together.
269
+
270
+ Args:
271
+ other (FlexFloat | float | int): The other FlexFloat instance to add.
272
+ Returns:
273
+ FlexFloat: A new FlexFloat instance representing the sum.
274
+ """
275
+ if isinstance(other, Number):
276
+ other = FlexFloat.from_float(other)
277
+ if not isinstance(other, FlexFloat):
278
+ raise TypeError("Can only add FlexFloat instances.")
279
+
280
+ if self.sign != other.sign:
281
+ return self - (-other)
282
+
283
+ # OBJECTIVE: Add two FlexFloat instances together.
284
+ # https://www.sciencedirect.com/topics/computer-science/floating-point-addition
285
+ # and: https://cse.hkust.edu.hk/~cktang/cs180/notes/lec21.pdf
286
+ #
287
+ # Steps:
288
+ # 0. Handle special cases (NaN, Infinity).
289
+ # 1. Extract exponent and fraction bits.
290
+ # 2. Prepend leading 1 to form the mantissa.
291
+ # 3. Compare exponents.
292
+ # 4. Shift smaller mantissa if necessary.
293
+ # 5. Add mantissas.
294
+ # 6. Normalize mantissa and adjust exponent if necessary.
295
+ # 7. Grow exponent if necessary.
296
+ # 8. Round result.
297
+ # 9. Return new FlexFloat instance.
298
+
299
+ # Step 0: Handle special cases
300
+ if self.is_zero() or other.is_zero():
301
+ return self.copy() if other.is_zero() else other.copy()
302
+
303
+ if self.is_nan() or other.is_nan():
304
+ return FlexFloat.nan()
305
+
306
+ if self.is_infinity() and other.is_infinity():
307
+ return self.copy() if self.sign == other.sign else FlexFloat.nan()
308
+ if self.is_infinity() or other.is_infinity():
309
+ return self.copy() if self.is_infinity() else other.copy()
310
+
311
+ # Step 1: Extract exponent and fraction bits
312
+ exponent_self = self.exponent.to_signed_int() + 1
313
+ exponent_other = other.exponent.to_signed_int() + 1
314
+
315
+ # Step 2: Prepend leading 1 to form the mantissa
316
+ mantissa_self = [True] + self.fraction
317
+ mantissa_other = [True] + other.fraction
318
+
319
+ # Step 3: Compare exponents (self is always larger or equal)
320
+ if exponent_self < exponent_other:
321
+ exponent_self, exponent_other = exponent_other, exponent_self
322
+ mantissa_self, mantissa_other = mantissa_other, mantissa_self
323
+
324
+ # Step 4: Shift smaller mantissa if necessary
325
+ if exponent_self > exponent_other:
326
+ shift_amount = exponent_self - exponent_other
327
+ mantissa_other = mantissa_other.shift(shift_amount)
328
+
329
+ # Step 5: Add mantissas
330
+ assert (
331
+ len(mantissa_self) == 53
332
+ ), "Fraction must be 53 bits long. (1 leading bit + 52 fraction bits)"
333
+ assert len(mantissa_self) == len(mantissa_other), (
334
+ f"Mantissas must be the same length. Expected 53 bits, "
335
+ f"got {len(mantissa_other)} bits."
336
+ )
337
+
338
+ mantissa_result = BitArrayType.zeros(53) # 1 leading bit + 52 fraction bits
339
+ carry = False
340
+ for i in range(52, -1, -1):
341
+ total = mantissa_self[i] + mantissa_other[i] + carry
342
+ mantissa_result[i] = total % 2 == 1
343
+ carry = total > 1
344
+
345
+ # Step 6: Normalize mantissa and adjust exponent if necessary
346
+ # Only need to normalize if there is a carry
347
+ if carry:
348
+ # Insert the carry bit and shift right
349
+ mantissa_result = mantissa_result.shift(1, fill=True)
350
+ exponent_self += 1
351
+
352
+ # Step 7: Grow exponent if necessary
353
+ exp_result_length = self._grow_exponent(exponent_self, len(self.exponent))
354
+ assert (
355
+ exponent_self - (1 << (exp_result_length - 1)) < 2
356
+ ), "Exponent growth should not exceed 1 bit."
357
+
358
+ exponent_result = BitArrayType.from_signed_int(
359
+ exponent_self - 1, exp_result_length
360
+ )
361
+ return FlexFloat(
362
+ sign=self.sign,
363
+ exponent=exponent_result,
364
+ fraction=mantissa_result[1:], # Exclude leading bit
365
+ )
366
+
367
+ def __sub__(self, other: FlexFloat | Number) -> FlexFloat:
368
+ """Subtract one FlexFloat instance from another.
369
+
370
+ Args:
371
+ other (FlexFloat | float | int): The FlexFloat instance to subtract.
372
+ Returns:
373
+ FlexFloat: A new FlexFloat instance representing the difference.
374
+ """
375
+ if isinstance(other, Number):
376
+ other = FlexFloat.from_float(other)
377
+ if not isinstance(other, FlexFloat):
378
+ raise TypeError("Can only subtract FlexFloat instances.")
379
+
380
+ # If signs are different, subtraction becomes addition
381
+ if self.sign != other.sign:
382
+ return self + (-other)
383
+
384
+ # OBJECTIVE: Subtract two FlexFloat instances.
385
+ # Based on floating-point subtraction algorithms
386
+ #
387
+ # Steps:
388
+ # 0. Handle special cases (NaN, Infinity, zero).
389
+ # 1. Extract exponent and fraction bits.
390
+ # 2. Prepend leading 1 to form the mantissa.
391
+ # 3. Compare exponents and align mantissas.
392
+ # 4. Compare magnitudes to determine result sign.
393
+ # 5. Subtract mantissas (larger - smaller).
394
+ # 6. Normalize mantissa and adjust exponent if necessary.
395
+ # 7. Grow exponent if necessary.
396
+ # 8. Return new FlexFloat instance.
397
+
398
+ # Step 0: Handle special cases
399
+ if self.is_zero() or other.is_zero():
400
+ return self.copy() if other.is_zero() else -other.copy()
401
+
402
+ if self.is_nan() or other.is_nan():
403
+ return FlexFloat.nan()
404
+
405
+ if self.is_infinity() and other.is_infinity():
406
+ if self.sign == other.sign:
407
+ return FlexFloat.nan() # inf - inf = NaN
408
+ return self.copy() # inf - (-inf) = inf
409
+
410
+ if self.is_infinity():
411
+ return self.copy()
412
+
413
+ if other.is_infinity():
414
+ return -other
415
+
416
+ # Step 1: Extract exponent and fraction bits
417
+ exponent_self = self.exponent.to_signed_int() + 1
418
+ exponent_other = other.exponent.to_signed_int() + 1
419
+
420
+ # Step 2: Prepend leading 1 to form the mantissa
421
+ mantissa_self = [True] + self.fraction
422
+ mantissa_other = [True] + other.fraction
423
+
424
+ # Step 3: Align mantissas by shifting the smaller exponent
425
+ result_sign = self.sign
426
+ shift_amount = abs(exponent_self - exponent_other)
427
+ if exponent_self >= exponent_other:
428
+ mantissa_other = mantissa_other.shift(shift_amount)
429
+ result_exponent = exponent_self
430
+ else:
431
+ mantissa_self = mantissa_self.shift(shift_amount)
432
+ result_exponent = exponent_other
433
+
434
+ # Step 4: Compare magnitudes to determine which mantissa is larger
435
+ # Convert mantissas to integers for comparison
436
+ mantissa_self_int = mantissa_self.to_int()
437
+ mantissa_other_int = mantissa_other.to_int()
438
+
439
+ if mantissa_self_int >= mantissa_other_int:
440
+ larger_mantissa = mantissa_self
441
+ smaller_mantissa = mantissa_other
442
+ result_sign = self.sign
443
+ else:
444
+ larger_mantissa = mantissa_other
445
+ smaller_mantissa = mantissa_self
446
+ # Flip sign since we're computing -(smaller - larger)
447
+ result_sign = not self.sign
448
+
449
+ # Step 5: Subtract mantissas (larger - smaller)
450
+ assert (
451
+ len(larger_mantissa) == 53
452
+ ), "Mantissa must be 53 bits long. (1 leading bit + 52 fraction bits)"
453
+ assert len(larger_mantissa) == len(smaller_mantissa), (
454
+ f"Mantissas must be the same length. Expected 53 bits, "
455
+ f"got {len(smaller_mantissa)} bits."
456
+ )
457
+
458
+ mantissa_result = BitArrayType.zeros(53)
459
+ borrow = False
460
+ for i in range(52, -1, -1):
461
+ diff = int(larger_mantissa[i]) - int(smaller_mantissa[i]) - int(borrow)
462
+
463
+ mantissa_result[i] = diff % 2 == 1
464
+ borrow = diff < 0
465
+
466
+ assert not borrow, "Subtraction should not result in a negative mantissa."
467
+
468
+ # Step 6: Normalize mantissa and adjust exponent if necessary
469
+ # Find the first 1 bit (leading bit might have been canceled out)
470
+ leading_zero_count = next(
471
+ (i for i, bit in enumerate(mantissa_result) if bit), len(mantissa_result)
472
+ )
473
+
474
+ # Handle case where result becomes zero or denormalized
475
+ if leading_zero_count >= 53:
476
+ return FlexFloat.from_float(0.0)
477
+
478
+ if leading_zero_count > 0:
479
+ # Shift left to normalize
480
+ mantissa_result = mantissa_result.shift(-leading_zero_count)
481
+ result_exponent -= leading_zero_count
482
+
483
+ # Step 7: Grow exponent if necessary (handle underflow)
484
+ exp_result_length = self._grow_exponent(result_exponent, len(self.exponent))
485
+
486
+ exp_result = BitArrayType.from_signed_int(
487
+ result_exponent - 1, exp_result_length
488
+ )
489
+
490
+ return FlexFloat(
491
+ sign=result_sign,
492
+ exponent=exp_result,
493
+ fraction=mantissa_result[1:], # Exclude leading bit
494
+ )
495
+
496
+ def __mul__(self, other: FlexFloat | Number) -> FlexFloat:
497
+ """Multiply two FlexFloat instances together.
498
+
499
+ Args:
500
+ other (FlexFloat | float | int): The other FlexFloat instance to multiply.
501
+ Returns:
502
+ FlexFloat: A new FlexFloat instance representing the product.
503
+ """
504
+ if isinstance(other, Number):
505
+ other = FlexFloat.from_float(other)
506
+ if not isinstance(other, FlexFloat):
507
+ raise TypeError("Can only multiply FlexFloat instances.")
508
+
509
+ # OBJECTIVE: Multiply two FlexFloat instances together.
510
+ # https://www.rfwireless-world.com/tutorials/ieee-754-floating-point-arithmetic
511
+ #
512
+ # Steps:
513
+ # 0. Handle special cases (NaN, Infinity, zero).
514
+ # 1. Calculate result sign (XOR of operand signs).
515
+ # 2. Extract and add exponents (subtract bias).
516
+ # 3. Multiply mantissas.
517
+ # 4. Normalize mantissa and adjust exponent if necessary.
518
+ # 5. Check for overflow/underflow.
519
+ # 6. Grow exponent if necessary.
520
+ # 7. Return new FlexFloat instance.
521
+
522
+ # Step 0: Handle special cases
523
+ if self.is_nan() or other.is_nan():
524
+ return FlexFloat.nan()
525
+
526
+ if self.is_zero() or other.is_zero():
527
+ return FlexFloat.zero()
528
+
529
+ if self.is_infinity() or other.is_infinity():
530
+ result_sign = self.sign ^ other.sign
531
+ return FlexFloat.infinity(sign=result_sign)
532
+
533
+ # Step 1: Calculate result sign (XOR of signs)
534
+ result_sign = self.sign ^ other.sign
535
+
536
+ # Step 2: Extract exponent and fraction bits
537
+ # Note: The stored exponent needs +1 to get the actual value (like in addition)
538
+ exponent_self = self.exponent.to_signed_int() + 1
539
+ exponent_other = other.exponent.to_signed_int() + 1
540
+
541
+ # Step 3: Add exponents
542
+ # When multiplying, we add the unbiased exponents
543
+ result_exponent = exponent_self + exponent_other
544
+
545
+ # Step 4: Multiply mantissas
546
+ # Prepend leading 1 to form the mantissa (1.fraction)
547
+ mantissa_self = [True] + self.fraction
548
+ mantissa_other = [True] + other.fraction
549
+
550
+ # Convert mantissas to integers for multiplication
551
+ mantissa_self_int = mantissa_self.to_int()
552
+ mantissa_other_int = mantissa_other.to_int()
553
+
554
+ # Multiply the mantissas
555
+ product = mantissa_self_int * mantissa_other_int
556
+
557
+ # Convert back to bit array
558
+ # The product will have up to 106 bits (53 + 53)
559
+ if product == 0:
560
+ return FlexFloat.zero()
561
+
562
+ product_bits = BitArrayType.zeros(106)
563
+ for i in range(105, -1, -1):
564
+ product_bits[i] = product & 1 == 1
565
+ product >>= 1
566
+ if product <= 0:
567
+ break
568
+
569
+ # Step 5: Normalize mantissa and adjust exponent if necessary
570
+ # Find the position of the most significant bit
571
+ msb_position = next((i for i, bit in enumerate(product_bits) if bit), None)
572
+
573
+ assert msb_position is not None, "Product should not be zero here."
574
+
575
+ # The mantissa multiplication gives us a result with 2 integer bits
576
+ # We need to normalize to have exactly 1 integer bit
577
+ # If MSB is at position 0, we have a 2-bit integer part (11.xxxxx)
578
+ # If MSB is at position 1, we have a 1-bit integer part (1.xxxxx)
579
+ if msb_position == 0:
580
+ result_exponent += 1
581
+ normalized_mantissa = product_bits[msb_position : msb_position + 53]
582
+
583
+ # Pad with zeros if we don't have enough bits
584
+ missing_bits = 53 - len(normalized_mantissa)
585
+ if missing_bits > 0:
586
+ normalized_mantissa += [False] * missing_bits
587
+
588
+ # Step 6: Grow exponent if necessary to accommodate the result
589
+ exp_result_length = max(len(self.exponent), len(other.exponent))
590
+
591
+ # Check if we need to grow the exponent to accommodate the result
592
+ exp_result_length = self._grow_exponent(result_exponent, exp_result_length)
593
+
594
+ exp_result = BitArrayType.from_signed_int(
595
+ result_exponent - 1, exp_result_length
596
+ )
597
+
598
+ return FlexFloat(
599
+ sign=result_sign,
600
+ exponent=exp_result,
601
+ fraction=normalized_mantissa[1:], # Exclude leading bit
602
+ )
603
+
604
+ def __rmul__(self, other: Number) -> FlexFloat:
605
+ """Right-hand multiplication for Number types.
606
+
607
+ Args:
608
+ other (float | int): The number to multiply with this FlexFloat.
609
+ Returns:
610
+ FlexFloat: A new FlexFloat instance representing the product.
611
+ """
612
+ return self * other
613
+
614
+ def __truediv__(self, other: FlexFloat | Number) -> FlexFloat:
615
+ """Divide this FlexFloat by another FlexFloat or number.
616
+
617
+ Args:
618
+ other (FlexFloat | float | int): The divisor.
619
+ Returns:
620
+ FlexFloat: A new FlexFloat instance representing the quotient.
621
+ """
622
+ if isinstance(other, Number):
623
+ other = FlexFloat.from_float(other)
624
+ if not isinstance(other, FlexFloat):
625
+ raise TypeError("Can only divide FlexFloat instances.")
626
+
627
+ # OBJECTIVE: Divide two FlexFloat instances.
628
+ # https://www.rfwireless-world.com/tutorials/ieee-754-floating-point-arithmetic
629
+ #
630
+ # Steps:
631
+ # 0. Handle special cases (NaN, Infinity, zero).
632
+ # 1. Calculate result sign (XOR of operand signs).
633
+ # 2. Extract and subtract exponents (add bias).
634
+ # 3. Divide mantissas.
635
+ # 4. Normalize mantissa and adjust exponent if necessary.
636
+ # 5. Check for overflow/underflow.
637
+ # 6. Grow exponent if necessary.
638
+ # 7. Return new FlexFloat instance.
639
+
640
+ # Step 0: Handle special cases
641
+ if self.is_nan() or other.is_nan():
642
+ return FlexFloat.nan()
643
+
644
+ # Zero cases
645
+ if self.is_zero() and other.is_zero():
646
+ return FlexFloat.nan() # 0 / 0 = NaN
647
+ if self.is_zero() and not other.is_zero():
648
+ return FlexFloat.zero() # 0 / finite = 0
649
+ if not self.is_zero() and other.is_zero():
650
+ return FlexFloat.infinity(sign=self.sign ^ other.sign) # finite / 0 = inf
651
+
652
+ # Infinity cases
653
+ if self.is_infinity() and other.is_infinity():
654
+ return FlexFloat.nan() # inf / inf = NaN
655
+ if self.is_infinity():
656
+ return FlexFloat.infinity(sign=self.sign ^ other.sign) # inf / finite = inf
657
+ if other.is_infinity():
658
+ return FlexFloat.zero() # finite / inf = 0
659
+
660
+ # Step 1: Calculate result sign (XOR of signs)
661
+ result_sign = self.sign ^ other.sign
662
+
663
+ # Step 2: Extract exponent and fraction bits
664
+ # Note: The stored exponent needs +1 to get the actual value
665
+ # (like in multiplication)
666
+ exponent_self = self.exponent.to_signed_int() + 1
667
+ exponent_other = other.exponent.to_signed_int() + 1
668
+
669
+ # Step 3: Subtract exponents (for division, we subtract the divisor's exponent)
670
+ result_exponent = exponent_self - exponent_other
671
+
672
+ # Step 4: Divide mantissas
673
+ # Prepend leading 1 to form the mantissa (1.fraction)
674
+ mantissa_self = [True] + self.fraction
675
+ mantissa_other = [True] + other.fraction
676
+
677
+ # Convert mantissas to integers for division
678
+ mantissa_self_int = mantissa_self.to_int()
679
+ mantissa_other_int = mantissa_other.to_int()
680
+
681
+ # Normalize mantissa for division (avoid overflow) -> scale the dividend
682
+ if mantissa_self_int < mantissa_other_int:
683
+ scale_factor = 1 << 53
684
+ result_exponent -= 1 # Adjust exponent since result < 1.0
685
+ else:
686
+ scale_factor = 1 << 52
687
+ scaled_dividend = mantissa_self_int * scale_factor
688
+ quotient = scaled_dividend // mantissa_other_int
689
+
690
+ if quotient == 0:
691
+ return FlexFloat.zero()
692
+
693
+ # Convert quotient to BitArray for easier bit manipulation
694
+ quotient_bitarray = BitArrayType.zeros(64) # Use a fixed size for consistency
695
+ temp_quotient = quotient
696
+ bit_pos = 63
697
+ while temp_quotient > 0 and bit_pos >= 0:
698
+ quotient_bitarray[bit_pos] = (temp_quotient & 1) == 1
699
+ temp_quotient >>= 1
700
+ bit_pos -= 1
701
+
702
+ # Step 5: Normalize mantissa and adjust exponent if necessary
703
+ # Find the position of the most significant bit (first 1)
704
+ msb_pos = next((i for i, bit in enumerate(quotient_bitarray) if bit), None)
705
+
706
+ if msb_pos is None:
707
+ return FlexFloat.zero()
708
+
709
+ # Extract exactly 53 bits starting from the MSB (1 integer + 52 fraction)
710
+ normalized_mantissa = quotient_bitarray[msb_pos : msb_pos + 53]
711
+ normalized_mantissa = normalized_mantissa.shift(
712
+ 53 - len(normalized_mantissa), fill=False
713
+ )
714
+
715
+ # Step 6: Grow exponent if necessary to accommodate the result
716
+ exp_result_length = max(len(self.exponent), len(other.exponent))
717
+
718
+ # Check if we need to grow the exponent to accommodate the result
719
+ exp_result_length = self._grow_exponent(result_exponent, exp_result_length)
720
+
721
+ exp_result = BitArrayType.from_signed_int(
722
+ result_exponent - 1, exp_result_length
723
+ )
724
+
725
+ return FlexFloat(
726
+ sign=result_sign,
727
+ exponent=exp_result,
728
+ fraction=normalized_mantissa[1:], # Exclude leading bit
729
+ )
730
+
731
+ def __rtruediv__(self, other: Number) -> FlexFloat:
732
+ """Right-hand division for Number types.
733
+
734
+ Args:
735
+ other (float | int): The number to divide by this FlexFloat.
736
+ Returns:
737
+ FlexFloat: A new FlexFloat instance representing the quotient.
738
+ """
739
+ return FlexFloat.from_float(other) / self
740
+
741
+ def __abs__(self) -> FlexFloat:
742
+ """Return the absolute value of the FlexFloat instance.
743
+
744
+ Returns:
745
+ FlexFloat: A new FlexFloat instance with the same exponent and fraction,
746
+ but with the sign set to False (positive).
747
+ """
748
+ return FlexFloat(
749
+ sign=False,
750
+ exponent=self.exponent.copy(),
751
+ fraction=self.fraction.copy(),
752
+ )
753
+
754
+ def abs(self) -> FlexFloat:
755
+ """Calculate the absolute value of the FlexFloat instance.
756
+
757
+ Returns:
758
+ FlexFloat: A new FlexFloat instance with the same exponent and fraction,
759
+ but with the sign set to False (positive).
760
+ """
761
+ return abs(self)
762
+
763
+ def exp(self) -> FlexFloat:
764
+ """Calculate the exponential of the FlexFloat instance.
765
+
766
+ Returns:
767
+ FlexFloat: A new FlexFloat instance representing e raised to the power
768
+ of this instance.
769
+ """
770
+ # Use Python's math.exp for the base calculation
771
+ return FlexFloat.e**self
772
+
773
+ def __pow__(self, other: FlexFloat | Number) -> FlexFloat:
774
+ """Power operation for FlexFloat instances.
775
+
776
+ Args:
777
+ other (FlexFloat | Number): The exponent.
778
+ Returns:
779
+ FlexFloat: A new FlexFloat instance representing the power.
780
+ """
781
+ if isinstance(other, Number):
782
+ other = FlexFloat.from_float(other)
783
+ if not isinstance(other, FlexFloat):
784
+ raise TypeError("Can only raise FlexFloat instances to a power.")
785
+
786
+ # OBJECTIVE: Compute self^other using the identity: a^b = exp(b * ln(a))
787
+ # For most cases, we use logarithmic computation for generality
788
+ # Handle special cases first for performance and correctness
789
+ #
790
+ # Steps:
791
+ # 0. Handle special cases (NaN, infinity, zero, one)
792
+ # 1. Handle negative base with fractional exponent (returns NaN)
793
+ # 2. For general case: compute ln(|base|) * exponent
794
+ # 3. Compute exp(result) to get the final power
795
+ # 4. Handle sign for negative base with integer exponent
796
+ # 5. Return new FlexFloat instance
797
+
798
+ # Step 0: Handle special cases
799
+ if self.is_nan() or other.is_nan():
800
+ return FlexFloat.nan()
801
+
802
+ # Zero base cases
803
+ if self.is_zero():
804
+ if other.is_zero():
805
+ return FlexFloat.from_float(1.0) # 0^0 = 1 (by convention)
806
+ if other.sign: # negative exponent
807
+ return FlexFloat.infinity(sign=False) # 0^(-n) = +infinity
808
+ return FlexFloat.zero() # 0^n = 0 for positive n
809
+
810
+ # One base: 1^anything = 1
811
+ if (
812
+ not self.sign
813
+ and self.exponent.to_signed_int() == -1
814
+ and not any(self.fraction)
815
+ ):
816
+ # Check if self is exactly 1.0 (exponent = -1, fraction = 0)
817
+ return FlexFloat.from_float(1.0)
818
+
819
+ # Zero exponent: anything^0 = 1 (except 0^0 handled above)
820
+ if other.is_zero():
821
+ return FlexFloat.from_float(1.0)
822
+
823
+ # One exponent: anything^1 = anything
824
+ if (
825
+ not other.sign
826
+ and other.exponent.to_signed_int() == -1
827
+ and not any(other.fraction)
828
+ ):
829
+ # Check if other is exactly 1.0 (exponent = -1, fraction = 0)
830
+ return self.copy()
831
+
832
+ # Infinity cases
833
+ if self.is_infinity():
834
+ if other.is_zero():
835
+ return FlexFloat.from_float(1.0) # inf^0 = 1
836
+ if other.sign: # negative exponent
837
+ return FlexFloat.zero() # inf^(-n) = 0
838
+ # inf^n = inf, but need to handle sign for negative infinity
839
+ if not self.sign: # positive infinity
840
+ return FlexFloat.infinity(sign=False) # (+inf)^n = +inf
841
+
842
+ # Check if exponent is an integer for sign determination
843
+ try:
844
+ exp_float = other.to_float()
845
+ if exp_float == int(exp_float): # integer exponent
846
+ result_sign = int(exp_float) % 2 == 1 # odd = negative
847
+ else:
848
+ return FlexFloat.nan() # (-inf)^(non-integer) = NaN
849
+ except (ValueError, OverflowError):
850
+ # For very large exponents, assume positive result
851
+ result_sign = False
852
+ return FlexFloat.infinity(sign=result_sign)
853
+
854
+ if other.is_infinity():
855
+ try:
856
+ base_abs = abs(self.to_float())
857
+ if base_abs == 1.0:
858
+ return FlexFloat.from_float(1.0) # 1^inf = 1
859
+ if base_abs > 1.0:
860
+ return (
861
+ FlexFloat.infinity(sign=False)
862
+ if not other.sign
863
+ else FlexFloat.zero()
864
+ )
865
+ # base_abs < 1.0
866
+ return (
867
+ FlexFloat.zero()
868
+ if not other.sign
869
+ else FlexFloat.infinity(sign=False)
870
+ )
871
+ except (ValueError, OverflowError):
872
+ # For very large/small bases, use log-based approach
873
+ pass
874
+
875
+ # Step 1: Handle negative base with fractional exponent
876
+ if self.sign:
877
+ try:
878
+ # Check if exponent is an integer
879
+ exp_float = other.to_float()
880
+ if exp_float != int(exp_float):
881
+ return FlexFloat.nan() # (-base)^(non-integer) = NaN
882
+ is_odd_exponent = int(exp_float) % 2 == 1
883
+ except (ValueError, OverflowError):
884
+ # For very large exponents, we can't easily determine if integer
885
+ # Conservative approach: return NaN for negative base
886
+ return FlexFloat.nan()
887
+ else:
888
+ is_odd_exponent = False
889
+
890
+ # Step 2-3: General case using a^b = exp(b * ln(a))
891
+ # We need to compute ln(|self|) * other, then exp of that result
892
+
893
+ # For the logarithmic approach, we work with the absolute value
894
+ abs_base = self.abs()
895
+
896
+ # Use the mathematical identity: a^b = exp(b * ln(a))
897
+ # However, since we don't have ln implemented, we'll use Python's math
898
+ # for the core calculation and then convert back to FlexFloat
899
+ try:
900
+ # Convert to float for the mathematical computation
901
+ base_float = abs_base.to_float()
902
+ exp_float = other.to_float()
903
+
904
+ # For very small results that would underflow in float arithmetic,
905
+ # we need to use extended precision
906
+ log_result_estimate = exp_float * math.log(base_float)
907
+
908
+ # Check if the result would be too small for standard float
909
+ if log_result_estimate < -700: # This would underflow to 0 in float
910
+ # Use extended precision approach
911
+ # Estimate the required exponent bits for the very small result
912
+ estimated_exp = int(log_result_estimate / LOG10_2)
913
+ required_exp_bits = max(11, abs(estimated_exp).bit_length() + 2)
914
+
915
+ # Create a small result with extended exponent
916
+ small_exp = BitArrayType.from_signed_int(
917
+ estimated_exp, required_exp_bits
918
+ )
919
+ # Use a normalized mantissa (leading 1 + some fraction bits)
920
+ result = FlexFloat(
921
+ sign=False, exponent=small_exp, fraction=BitArrayType.ones(52)
922
+ )
923
+ else:
924
+ # Compute the power using Python's built-in pow
925
+ result_float = pow(base_float, exp_float)
926
+
927
+ # Handle potential overflow/underflow
928
+ if math.isinf(result_float):
929
+ return FlexFloat.infinity(sign=False)
930
+ if result_float == 0.0:
931
+ # Even if Python returns 0, we might want extended precision
932
+ if log_result_estimate < -300: # Very small but not quite underflow
933
+ estimated_exp = int(log_result_estimate / LOG10_2)
934
+ required_exp_bits = max(11, abs(estimated_exp).bit_length() + 2)
935
+ small_exp = BitArrayType.from_signed_int(
936
+ estimated_exp, required_exp_bits
937
+ )
938
+ result = FlexFloat(
939
+ sign=False,
940
+ exponent=small_exp,
941
+ fraction=BitArrayType.ones(52),
942
+ )
943
+ else:
944
+ return FlexFloat.zero()
945
+ if math.isnan(result_float):
946
+ return FlexFloat.nan()
947
+
948
+ # Convert result back to FlexFloat
949
+ result = FlexFloat.from_float(result_float)
950
+
951
+ except (ValueError, OverflowError):
952
+ # If float computation overflows, use extended precision approach
953
+ # This is a fallback for when the result is too large for float
954
+
955
+ # For very large results, we estimate the exponent growth needed
956
+ try:
957
+ # Estimate log(result) = exponent * log(base)
958
+ log_base = (
959
+ math.log(abs(self.to_float()))
960
+ if not self.is_zero()
961
+ else -float("inf")
962
+ )
963
+ exp_float = other.to_float()
964
+ estimated_log_result = exp_float * log_base
965
+
966
+ # Estimate the required exponent bits
967
+ estimated_exp_float = estimated_log_result / LOG10_2
968
+ required_exp_bits = max(
969
+ 11, int(abs(estimated_exp_float)).bit_length() + 2
970
+ )
971
+
972
+ # Create a result with extended exponent
973
+ # This is a simplified approach - a full implementation would
974
+ # use arbitrary precision arithmetic
975
+ if estimated_exp_float > 0:
976
+ # Very large positive result
977
+ large_exp = BitArrayType.from_signed_int(
978
+ int(estimated_exp_float), required_exp_bits
979
+ )
980
+ result = FlexFloat(
981
+ sign=False, exponent=large_exp, fraction=BitArrayType.ones(52)
982
+ )
983
+ else:
984
+ # Very small positive result
985
+ small_exp = BitArrayType.from_signed_int(
986
+ int(estimated_exp_float), required_exp_bits
987
+ )
988
+ result = FlexFloat(
989
+ sign=False, exponent=small_exp, fraction=BitArrayType.ones(52)
990
+ )
991
+
992
+ except (ValueError, OverflowError, ZeroDivisionError):
993
+ # Ultimate fallback
994
+ return FlexFloat.nan()
995
+
996
+ # Step 4: Handle sign for negative base with integer exponent
997
+ if self.sign and is_odd_exponent:
998
+ result.sign = True
999
+
1000
+ return result
1001
+
1002
+ def __rpow__(self, base: Number) -> FlexFloat:
1003
+ """Right-hand power operation for Number types.
1004
+
1005
+ Args:
1006
+ base (float | int): The base to raise to the power of this FlexFloat.
1007
+ Returns:
1008
+ FlexFloat: A new FlexFloat instance representing the power.
1009
+ """
1010
+ return FlexFloat.from_float(base) ** self
1011
+
1012
+
1013
+ # Initialize class variable after class definition
1014
+ FlexFloat.e = FlexFloat.from_float(math.e)