flexfloat 0.1.0__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/__init__.py ADDED
@@ -0,0 +1,13 @@
1
+ """FlexFloat - A library for arbitrary precision floating point arithmetic.
2
+
3
+ This package provides the FlexFloat class for handling floating-point numbers
4
+ with growable exponents and fixed-size fractions.
5
+ """
6
+
7
+ from .bitarray import BitArray
8
+ from .core import FlexFloat
9
+
10
+ __version__ = "0.1.0"
11
+ __author__ = "Ferran Sanchez Llado"
12
+
13
+ __all__ = ["FlexFloat", "BitArray"]
flexfloat/bitarray.py ADDED
@@ -0,0 +1,253 @@
1
+ """BitArray implementation for the flexfloat package."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import struct
6
+ from typing import Iterator, overload
7
+
8
+
9
+ class BitArray:
10
+ """A bit array class that encapsulates a list of booleans with utility methods.
11
+
12
+ This class provides all the functionality previously available through utility functions,
13
+ now encapsulated as methods for better object-oriented design.
14
+ """
15
+
16
+ def __init__(self, bits: list[bool] | None = None):
17
+ """Initialize a BitArray.
18
+
19
+ Args:
20
+ bits: Initial list of boolean values. Defaults to empty list.
21
+ """
22
+ self._bits = bits if bits is not None else []
23
+
24
+ @classmethod
25
+ def from_float(cls, value: float) -> BitArray:
26
+ """Convert a floating-point number to a bit array.
27
+
28
+ Args:
29
+ value (float): The floating-point number to convert.
30
+ Returns:
31
+ BitArray: A BitArray representing the bits of the floating-point number.
32
+ """
33
+ # Pack as double precision (64 bits)
34
+ packed = struct.pack("!d", value)
35
+ # Convert to boolean list
36
+ bits = [bool((byte >> bit) & 1) for byte in packed for bit in range(7, -1, -1)]
37
+ return cls(bits)
38
+
39
+ @classmethod
40
+ def from_signed_int(cls, value: int, length: int) -> BitArray:
41
+ """Convert a signed integer to a bit array using off-set binary representation.
42
+
43
+ Args:
44
+ value (int): The signed integer to convert.
45
+ length (int): The length of the resulting bit array.
46
+ Returns:
47
+ BitArray: A BitArray representing the bits of the signed integer.
48
+ Raises:
49
+ AssertionError: If the value is out of range for the specified length.
50
+ """
51
+ half = 1 << (length - 1)
52
+ max_value = half - 1
53
+ min_value = -half
54
+
55
+ assert (
56
+ min_value <= value <= max_value
57
+ ), "Value out of range for specified length."
58
+
59
+ # Convert to unsigned integer representation
60
+ unsigned_value = value - half
61
+
62
+ bits = [(unsigned_value >> i) & 1 == 1 for i in range(length - 1, -1, -1)]
63
+ return cls(bits)
64
+
65
+ @classmethod
66
+ def zeros(cls, length: int) -> BitArray:
67
+ """Create a BitArray filled with zeros.
68
+
69
+ Args:
70
+ length: The length of the bit array.
71
+ Returns:
72
+ BitArray: A BitArray filled with False values.
73
+ """
74
+ return cls([False] * length)
75
+
76
+ @classmethod
77
+ def ones(cls, length: int) -> BitArray:
78
+ """Create a BitArray filled with ones.
79
+
80
+ Args:
81
+ length: The length of the bit array.
82
+ Returns:
83
+ BitArray: A BitArray filled with True values.
84
+ """
85
+ return cls([True] * length)
86
+
87
+ @staticmethod
88
+ def parse_bitarray(bitstring: str) -> BitArray:
89
+ """Parse a string of bits (with optional spaces) into a BitArray instance."""
90
+ bits = [c == "1" for c in bitstring if c in "01"]
91
+ return BitArray(bits)
92
+
93
+ def to_float(self) -> float:
94
+ """Convert a 64-bit array to a floating-point number.
95
+
96
+ Returns:
97
+ float: The floating-point number represented by the bit array.
98
+ Raises:
99
+ AssertionError: If the bit array is not 64 bits long.
100
+ """
101
+ assert len(self._bits) == 64, "Bit array must be 64 bits long."
102
+
103
+ byte_values = bytearray()
104
+ for i in range(0, 64, 8):
105
+ byte = 0
106
+ for j in range(8):
107
+ if self._bits[i + j]:
108
+ byte |= 1 << (7 - j)
109
+ byte_values.append(byte)
110
+ # Unpack as double precision (64 bits)
111
+ return struct.unpack("!d", bytes(byte_values))[0] # type: ignore
112
+
113
+ def to_int(self) -> int:
114
+ """Convert the bit array to an unsigned integer.
115
+
116
+ Returns:
117
+ int: The integer represented by the bit array.
118
+ """
119
+ return sum((1 << i) for i, bit in enumerate(reversed(self._bits)) if bit)
120
+
121
+ def to_signed_int(self) -> int:
122
+ """Convert a bit array into a signed integer using off-set binary representation.
123
+
124
+ Returns:
125
+ int: The signed integer represented by the bit array.
126
+ Raises:
127
+ AssertionError: If the bit array is empty.
128
+ """
129
+ assert len(self._bits) > 0, "Bit array must not be empty."
130
+
131
+ int_value = self.to_int()
132
+ # Half of the maximum value
133
+ bias = 1 << (len(self._bits) - 1)
134
+ # If the sign bit is set, subtract the bias
135
+ return int_value - bias
136
+
137
+ def shift(self, shift_amount: int, fill: bool = False) -> BitArray:
138
+ """Shift the bit array left or right by a specified number of bits.
139
+
140
+ This function shifts the bits in the array, filling in new bits with the specified fill value.
141
+ If the shift is positive, it shifts left and fills with the fill value at the end.
142
+ If the shift is negative, it shifts right and fills with the fill value at the start.
143
+
144
+ Args:
145
+ shift_amount (int): The number of bits to shift. Positive for left shift, negative for right shift.
146
+ fill (bool): The value to fill in the new bits created by the shift. Defaults to False.
147
+ Returns:
148
+ BitArray: A new BitArray with the bits shifted and filled.
149
+ """
150
+ if shift_amount == 0:
151
+ return self.copy()
152
+ if abs(shift_amount) > len(self._bits):
153
+ new_bits = [fill] * len(self._bits)
154
+ elif shift_amount > 0:
155
+ new_bits = [fill] * shift_amount + self._bits[:-shift_amount]
156
+ else:
157
+ new_bits = self._bits[-shift_amount:] + [fill] * (-shift_amount)
158
+ return BitArray(new_bits)
159
+
160
+ def copy(self) -> BitArray:
161
+ """Create a copy of the bit array.
162
+
163
+ Returns:
164
+ BitArray: A new BitArray with the same bits.
165
+ """
166
+ return BitArray(self._bits.copy())
167
+
168
+ def __len__(self) -> int:
169
+ """Return the length of the bit array."""
170
+ return len(self._bits)
171
+
172
+ @overload
173
+ def __getitem__(self, index: int) -> bool: ...
174
+ @overload
175
+ def __getitem__(self, index: slice) -> BitArray: ...
176
+
177
+ def __getitem__(self, index: int | slice) -> bool | BitArray:
178
+ """Get an item or slice from the bit array."""
179
+ if isinstance(index, slice):
180
+ return BitArray(self._bits[index])
181
+ return self._bits[index]
182
+
183
+ @overload
184
+ def __setitem__(self, index: int, value: bool) -> None: ...
185
+ @overload
186
+ def __setitem__(self, index: slice, value: BitArray | list[bool]) -> None: ...
187
+
188
+ def __setitem__(
189
+ self, index: int | slice, value: bool | list[bool] | BitArray
190
+ ) -> None:
191
+ """Set an item or slice in the bit array."""
192
+ if isinstance(index, slice):
193
+ if isinstance(value, BitArray):
194
+ self._bits[index] = value._bits
195
+ elif isinstance(value, list):
196
+ self._bits[index] = value
197
+ else:
198
+ raise TypeError("Cannot assign a single bool to a slice")
199
+ return
200
+ if isinstance(value, bool):
201
+ self._bits[index] = value
202
+ else:
203
+ raise TypeError("Cannot assign a list or BitArray to a single index")
204
+
205
+ def __iter__(self) -> Iterator[bool]:
206
+ """Iterate over the bits in the array."""
207
+ return iter(self._bits)
208
+
209
+ def __add__(self, other: BitArray | list[bool]) -> BitArray:
210
+ """Concatenate two bit arrays."""
211
+ if isinstance(other, BitArray):
212
+ return BitArray(self._bits + other._bits)
213
+ return BitArray(self._bits + other)
214
+
215
+ def __radd__(self, other: list[bool]) -> BitArray:
216
+ """Reverse concatenation with a list."""
217
+ return BitArray(other + self._bits)
218
+
219
+ def __eq__(self, other: object) -> bool:
220
+ """Check equality with another BitArray or list."""
221
+ if isinstance(other, BitArray):
222
+ return self._bits == other._bits
223
+ if isinstance(other, list):
224
+ return self._bits == other
225
+ return False
226
+
227
+ def __bool__(self) -> bool:
228
+ """Return True if any bit is set."""
229
+ return any(self._bits)
230
+
231
+ def __repr__(self) -> str:
232
+ """Return a string representation of the BitArray."""
233
+ return f"BitArray({self._bits})"
234
+
235
+ def __str__(self) -> str:
236
+ """Return a string representation of the bits."""
237
+ return "".join("1" if bit else "0" for bit in self._bits)
238
+
239
+ def any(self) -> bool:
240
+ """Return True if any bit is set to True."""
241
+ return any(self._bits)
242
+
243
+ def all(self) -> bool:
244
+ """Return True if all bits are set to True."""
245
+ return all(self._bits)
246
+
247
+ def count(self, value: bool = True) -> int:
248
+ """Count the number of bits set to the specified value."""
249
+ return self._bits.count(value)
250
+
251
+ def reverse(self) -> BitArray:
252
+ """Return a new BitArray with the bits in reverse order."""
253
+ return BitArray(self._bits[::-1])
flexfloat/core.py ADDED
@@ -0,0 +1,678 @@
1
+ """Core FlexFloat class implementation."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from .bitarray import BitArray
6
+ from .types import Number
7
+
8
+
9
+ class FlexFloat:
10
+ """A class to represent a floating-point number with growable exponent and fixed-size fraction.
11
+ This class is designed to handle very large or very small numbers by adjusting the exponent dynamically.
12
+ While keeping the mantissa (fraction) fixed in size.
13
+
14
+ This class follows the IEEE 754 double-precision floating-point format,
15
+ but extends it to allow for a growable exponent and a fixed-size fraction.
16
+
17
+ Attributes:
18
+ sign (bool): The sign of the number (True for negative, False for positive).
19
+ exponent (BitArray): A growable bit array representing the exponent (uses off-set binary representation).
20
+ fraction (BitArray): A fixed-size bit array representing the fraction (mantissa) of the number.
21
+ """
22
+
23
+ def __init__(
24
+ self,
25
+ sign: bool = False,
26
+ exponent: BitArray | None = None,
27
+ fraction: BitArray | None = None,
28
+ ):
29
+ """Initialize a FlexFloat instance.
30
+
31
+ Args:
32
+ sign (bool): The sign of the number (True for negative, False for positive).
33
+ exponent (BitArray | None): The exponent bit array. If None, defaults to a zero exponent.
34
+ fraction (BitArray | None): The fraction bit array. If None, defaults to a zero fraction.
35
+ """
36
+ self.sign = sign
37
+ self.exponent = exponent if exponent is not None else BitArray.zeros(11)
38
+ self.fraction = fraction if fraction is not None else BitArray.zeros(52)
39
+
40
+ @classmethod
41
+ def from_float(cls, value: Number) -> FlexFloat:
42
+ """Create a FlexFloat instance from a number.
43
+
44
+ Args:
45
+ value (Number): The number to convert to FlexFloat.
46
+ Returns:
47
+ FlexFloat: A new FlexFloat instance representing the number.
48
+ """
49
+ value = float(value)
50
+ bits = BitArray.from_float(value)
51
+
52
+ return cls(sign=bits[0], exponent=bits[1:12], fraction=bits[12:64])
53
+
54
+ def to_float(self) -> float:
55
+ """Convert the FlexFloat instance back to a 64-bit float.
56
+
57
+ If float is bigger than 64 bits, it will truncate the value to fit into a 64-bit float.
58
+
59
+ Returns:
60
+ float: The floating-point number represented by the FlexFloat instance.
61
+ Raises:
62
+ ValueError: If the exponent or fraction lengths are not as expected.
63
+ """
64
+ if len(self.exponent) < 11 or len(self.fraction) < 52:
65
+ raise ValueError("Must be a standard 64-bit FlexFloat")
66
+
67
+ bits = BitArray([self.sign]) + self.exponent[:11] + self.fraction[:52]
68
+ return bits.to_float()
69
+
70
+ def __repr__(self) -> str:
71
+ """Return a string representation of the FlexFloat instance.
72
+
73
+ Returns:
74
+ str: A string representation of the FlexFloat instance.
75
+ """
76
+ return f"FlexFloat(sign={self.sign}, exponent={self.exponent}, fraction={self.fraction})"
77
+
78
+ def pretty(self) -> str:
79
+ """Return an easier to read string representation of the FlexFloat instance.
80
+ Mainly converts the exponent and fraction to integers for readability.
81
+
82
+ Returns:
83
+ str: A pretty string representation of the FlexFloat instance.
84
+ """
85
+ sign = "-" if self.sign else ""
86
+ exponent_value = self.exponent.to_signed_int() + 1
87
+ fraction_value = self.fraction.to_int()
88
+ return f"{sign}FlexFloat(exponent={exponent_value}, fraction={fraction_value})"
89
+
90
+ @classmethod
91
+ def nan(cls) -> FlexFloat:
92
+ """Create a FlexFloat instance representing NaN (Not a Number).
93
+
94
+ Returns:
95
+ FlexFloat: A new FlexFloat instance representing NaN.
96
+ """
97
+ exponent = BitArray.ones(11)
98
+ fraction = BitArray.ones(52)
99
+ return cls(sign=True, exponent=exponent, fraction=fraction)
100
+
101
+ @classmethod
102
+ def infinity(cls, sign: bool = False) -> FlexFloat:
103
+ """Create a FlexFloat instance representing Infinity.
104
+
105
+ Args:
106
+ sign (bool): The sign of the infinity (True for negative, False for positive).
107
+ Returns:
108
+ FlexFloat: A new FlexFloat instance representing Infinity.
109
+ """
110
+ exponent = BitArray.ones(11)
111
+ fraction = BitArray.zeros(52)
112
+ return cls(sign=sign, exponent=exponent, fraction=fraction)
113
+
114
+ @classmethod
115
+ def zero(cls) -> FlexFloat:
116
+ """Create a FlexFloat instance representing zero.
117
+
118
+ Returns:
119
+ FlexFloat: A new FlexFloat instance representing zero.
120
+ """
121
+ exponent = BitArray.zeros(11)
122
+ fraction = BitArray.zeros(52)
123
+ return cls(sign=False, exponent=exponent, fraction=fraction)
124
+
125
+ def _is_special_exponent(self) -> bool:
126
+ """Check if the exponent represents a special value (NaN or Infinity).
127
+
128
+ Returns:
129
+ bool: True if the exponent is at its maximum value, False otherwise.
130
+ """
131
+ # In IEEE 754, special values have all exponent bits set to 1
132
+ # This corresponds to the maximum value in the unsigned representation
133
+ # For signed offset binary, the maximum value is 2^(n-1) - 1 where n is the number of bits
134
+ max_signed_value = (1 << (len(self.exponent) - 1)) - 1
135
+ return self.exponent.to_signed_int() == max_signed_value
136
+
137
+ def is_nan(self) -> bool:
138
+ """Check if the FlexFloat instance represents NaN (Not a Number).
139
+
140
+ Returns:
141
+ bool: True if the FlexFloat instance is NaN, False otherwise.
142
+ """
143
+ return self._is_special_exponent() and any(self.fraction)
144
+
145
+ def is_infinity(self) -> bool:
146
+ """Check if the FlexFloat instance represents Infinity.
147
+
148
+ Returns:
149
+ bool: True if the FlexFloat instance is Infinity, False otherwise.
150
+ """
151
+ return self._is_special_exponent() and not any(self.fraction)
152
+
153
+ def is_zero(self) -> bool:
154
+ """Check if the FlexFloat instance represents zero.
155
+
156
+ Returns:
157
+ bool: True if the FlexFloat instance is zero, False otherwise.
158
+ """
159
+ return not any(self.exponent) and not any(self.fraction)
160
+
161
+ def copy(self) -> FlexFloat:
162
+ """Create a copy of the FlexFloat instance.
163
+
164
+ Returns:
165
+ FlexFloat: A new FlexFloat instance with the same sign, exponent, and fraction.
166
+ """
167
+ return FlexFloat(
168
+ sign=self.sign, exponent=self.exponent.copy(), fraction=self.fraction.copy()
169
+ )
170
+
171
+ def __str__(self) -> str:
172
+ """Float representation of the FlexFloat."""
173
+ sign = "-" if self.sign else ""
174
+
175
+ exponent_value = self.exponent.to_signed_int()
176
+ if exponent_value == 0:
177
+ return f"{sign}0.0"
178
+ max_exponent = 2 ** len(self.exponent) - 1
179
+ # Check NaN or Infinity
180
+ if exponent_value == max_exponent:
181
+ if any(self.fraction):
182
+ return f"{sign}NaN"
183
+ return f"{sign}Infinity"
184
+
185
+ fraction_value: float = 1
186
+ for i, bit in enumerate(self.fraction):
187
+ if bit:
188
+ fraction_value += 2 ** -(i + 1)
189
+
190
+ if exponent_value == 0:
191
+ return f"{sign}{fraction_value}.0"
192
+
193
+ # raise NotImplementedError("String representation for non-zero exponent not implemented yet.")
194
+ return ""
195
+
196
+ def __neg__(self) -> FlexFloat:
197
+ """Negate the FlexFloat instance."""
198
+ return FlexFloat(
199
+ sign=not self.sign,
200
+ exponent=self.exponent.copy(),
201
+ fraction=self.fraction.copy(),
202
+ )
203
+
204
+ @staticmethod
205
+ def _grow_exponent(exponent: int, exponent_length: int) -> int:
206
+ """Grow the exponent if it exceeds the maximum value for the current length.
207
+
208
+ Args:
209
+ exponent (int): The current exponent value.
210
+ exponent_length (int): The current length of the exponent in bits.
211
+ Returns:
212
+ int: The new exponent length if it needs to be grown, otherwise the same length.
213
+ """
214
+ while True:
215
+ half = 1 << (exponent_length - 1)
216
+ min_exponent = -half
217
+ max_exponent = half - 1
218
+
219
+ if min_exponent <= exponent <= max_exponent:
220
+ break
221
+ exponent_length += 1
222
+
223
+ return exponent_length
224
+
225
+ def __add__(self, other: FlexFloat | Number) -> FlexFloat:
226
+ """Add two FlexFloat instances together.
227
+
228
+ Args:
229
+ other (FlexFloat | float | int): The other FlexFloat instance to add.
230
+ Returns:
231
+ FlexFloat: A new FlexFloat instance representing the sum.
232
+ """
233
+ if isinstance(other, Number):
234
+ other = FlexFloat.from_float(other)
235
+ if not isinstance(other, FlexFloat):
236
+ raise TypeError("Can only add FlexFloat instances.")
237
+
238
+ if self.sign != other.sign:
239
+ return self - (-other)
240
+
241
+ # OBJECTIVE: Add two FlexFloat instances together.
242
+ # Based on: https://www.sciencedirect.com/topics/computer-science/floating-point-addition
243
+ # and: https://cse.hkust.edu.hk/~cktang/cs180/notes/lec21.pdf
244
+ #
245
+ # Steps:
246
+ # 0. Handle special cases (NaN, Infinity).
247
+ # 1. Extract exponent and fraction bits.
248
+ # 2. Prepend leading 1 to form the mantissa.
249
+ # 3. Compare exponents.
250
+ # 4. Shift smaller mantissa if necessary.
251
+ # 5. Add mantissas.
252
+ # 6. Normalize mantissa and adjust exponent if necessary.
253
+ # 7. Grow exponent if necessary.
254
+ # 8. Round result.
255
+ # 9. Return new FlexFloat instance.
256
+
257
+ # Step 0: Handle special cases
258
+ if self.is_zero() or other.is_zero():
259
+ return self.copy() if other.is_zero() else other.copy()
260
+
261
+ if self.is_nan() or other.is_nan():
262
+ return FlexFloat.nan()
263
+
264
+ if self.is_infinity() and other.is_infinity():
265
+ return self.copy() if self.sign == other.sign else FlexFloat.nan()
266
+ if self.is_infinity() or other.is_infinity():
267
+ return self.copy() if self.is_infinity() else other.copy()
268
+
269
+ # Step 1: Extract exponent and fraction bits
270
+ exponent_self = self.exponent.to_signed_int() + 1
271
+ exponent_other = other.exponent.to_signed_int() + 1
272
+
273
+ # Step 2: Prepend leading 1 to form the mantissa
274
+ mantissa_self = [True] + self.fraction
275
+ mantissa_other = [True] + other.fraction
276
+
277
+ # Step 3: Compare exponents (self is always larger or equal)
278
+ if exponent_self < exponent_other:
279
+ exponent_self, exponent_other = exponent_other, exponent_self
280
+ mantissa_self, mantissa_other = mantissa_other, mantissa_self
281
+
282
+ # Step 4: Shift smaller mantissa if necessary
283
+ if exponent_self > exponent_other:
284
+ shift_amount = exponent_self - exponent_other
285
+ mantissa_other = mantissa_other.shift(shift_amount)
286
+
287
+ # Step 5: Add mantissas
288
+ assert len(mantissa_self) == 53, "Fraction must be 53 bits long. (1 leading bit + 52 fraction bits)" # fmt: skip
289
+ assert len(mantissa_self) == len(mantissa_other), f"Mantissas must be the same length. Expected 53 bits, got {len(mantissa_other)} bits." # fmt: skip
290
+
291
+ mantissa_result = BitArray.zeros(53) # 1 leading bit + 52 fraction bits
292
+ carry = False
293
+ for i in range(52, -1, -1):
294
+ total = mantissa_self[i] + mantissa_other[i] + carry
295
+ mantissa_result[i] = total % 2 == 1
296
+ carry = total > 1
297
+
298
+ # Step 6: Normalize mantissa and adjust exponent if necessary
299
+ # Only need to normalize if there is a carry
300
+ if carry:
301
+ # Insert the carry bit and shift right
302
+ mantissa_result = mantissa_result.shift(1, fill=True)
303
+ exponent_self += 1
304
+
305
+ # Step 7: Grow exponent if necessary
306
+ exp_result_length = self._grow_exponent(exponent_self, len(self.exponent))
307
+ assert (
308
+ exponent_self - (1 << (exp_result_length - 1)) < 2
309
+ ), "Exponent growth should not exceed 1 bit."
310
+
311
+ exponent_result = BitArray.from_signed_int(exponent_self - 1, exp_result_length)
312
+ return FlexFloat(
313
+ sign=self.sign,
314
+ exponent=exponent_result,
315
+ fraction=mantissa_result[1:], # Exclude leading bit
316
+ )
317
+
318
+ def __sub__(self, other: FlexFloat | Number) -> FlexFloat:
319
+ """Subtract one FlexFloat instance from another.
320
+
321
+ Args:
322
+ other (FlexFloat | float | int): The FlexFloat instance to subtract.
323
+ Returns:
324
+ FlexFloat: A new FlexFloat instance representing the difference.
325
+ """
326
+ if isinstance(other, Number):
327
+ other = FlexFloat.from_float(other)
328
+ if not isinstance(other, FlexFloat):
329
+ raise TypeError("Can only subtract FlexFloat instances.")
330
+
331
+ # If signs are different, subtraction becomes addition
332
+ if self.sign != other.sign:
333
+ return self + (-other)
334
+
335
+ # OBJECTIVE: Subtract two FlexFloat instances.
336
+ # Based on floating-point subtraction algorithms
337
+ #
338
+ # Steps:
339
+ # 0. Handle special cases (NaN, Infinity, zero).
340
+ # 1. Extract exponent and fraction bits.
341
+ # 2. Prepend leading 1 to form the mantissa.
342
+ # 3. Compare exponents and align mantissas.
343
+ # 4. Compare magnitudes to determine result sign.
344
+ # 5. Subtract mantissas (larger - smaller).
345
+ # 6. Normalize mantissa and adjust exponent if necessary.
346
+ # 7. Grow exponent if necessary.
347
+ # 8. Return new FlexFloat instance.
348
+
349
+ # Step 0: Handle special cases
350
+ if self.is_zero() or other.is_zero():
351
+ return self.copy() if other.is_zero() else -other.copy()
352
+
353
+ if self.is_nan() or other.is_nan():
354
+ return FlexFloat.nan()
355
+
356
+ if self.is_infinity() and other.is_infinity():
357
+ if self.sign == other.sign:
358
+ return FlexFloat.nan() # inf - inf = NaN
359
+ return self.copy() # inf - (-inf) = inf
360
+
361
+ if self.is_infinity():
362
+ return self.copy()
363
+
364
+ if other.is_infinity():
365
+ return -other
366
+
367
+ # Step 1: Extract exponent and fraction bits
368
+ exponent_self = self.exponent.to_signed_int() + 1
369
+ exponent_other = other.exponent.to_signed_int() + 1
370
+
371
+ # Step 2: Prepend leading 1 to form the mantissa
372
+ mantissa_self = [True] + self.fraction
373
+ mantissa_other = [True] + other.fraction
374
+
375
+ # Step 3: Align mantissas by shifting the smaller exponent
376
+ result_sign = self.sign
377
+ shift_amount = abs(exponent_self - exponent_other)
378
+ if exponent_self >= exponent_other:
379
+ mantissa_other = mantissa_other.shift(shift_amount)
380
+ result_exponent = exponent_self
381
+ else:
382
+ mantissa_self = mantissa_self.shift(shift_amount)
383
+ result_exponent = exponent_other
384
+
385
+ # Step 4: Compare magnitudes to determine which mantissa is larger
386
+ # Convert mantissas to integers for comparison
387
+ mantissa_self_int = mantissa_self.to_int()
388
+ mantissa_other_int = mantissa_other.to_int()
389
+
390
+ if mantissa_self_int >= mantissa_other_int:
391
+ larger_mantissa = mantissa_self
392
+ smaller_mantissa = mantissa_other
393
+ result_sign = self.sign
394
+ else:
395
+ larger_mantissa = mantissa_other
396
+ smaller_mantissa = mantissa_self
397
+ # Flip sign since we're computing -(smaller - larger)
398
+ result_sign = not self.sign
399
+
400
+ # Step 5: Subtract mantissas (larger - smaller)
401
+ assert len(larger_mantissa) == 53, "Mantissa must be 53 bits long. (1 leading bit + 52 fraction bits)" # fmt: skip
402
+ assert len(larger_mantissa) == len(smaller_mantissa), f"Mantissas must be the same length. Expected 53 bits, got {len(smaller_mantissa)} bits." # fmt: skip
403
+
404
+ mantissa_result = BitArray.zeros(53)
405
+ borrow = False
406
+ for i in range(52, -1, -1):
407
+ diff = int(larger_mantissa[i]) - int(smaller_mantissa[i]) - int(borrow)
408
+
409
+ mantissa_result[i] = diff % 2 == 1
410
+ borrow = diff < 0
411
+
412
+ assert not borrow, "Subtraction should not result in a negative mantissa."
413
+
414
+ # Step 6: Normalize mantissa and adjust exponent if necessary
415
+ # Find the first 1 bit (leading bit might have been canceled out)
416
+ leading_zero_count = next(
417
+ (i for i, bit in enumerate(mantissa_result) if bit), len(mantissa_result)
418
+ )
419
+
420
+ # Handle case where result becomes zero or denormalized
421
+ if leading_zero_count >= 53:
422
+ return FlexFloat.from_float(0.0)
423
+
424
+ if leading_zero_count > 0:
425
+ # Shift left to normalize
426
+ mantissa_result = mantissa_result.shift(-leading_zero_count)
427
+ result_exponent -= leading_zero_count
428
+
429
+ # Step 7: Grow exponent if necessary (handle underflow)
430
+ exp_result_length = self._grow_exponent(result_exponent, len(self.exponent))
431
+
432
+ exp_result = BitArray.from_signed_int(result_exponent - 1, exp_result_length)
433
+
434
+ return FlexFloat(
435
+ sign=result_sign,
436
+ exponent=exp_result,
437
+ fraction=mantissa_result[1:], # Exclude leading bit
438
+ )
439
+
440
+ def __mul__(self, other: FlexFloat | Number) -> FlexFloat:
441
+ """Multiply two FlexFloat instances together.
442
+
443
+ Args:
444
+ other (FlexFloat | float | int): The other FlexFloat instance to multiply.
445
+ Returns:
446
+ FlexFloat: A new FlexFloat instance representing the product.
447
+ """
448
+ if isinstance(other, Number):
449
+ other = FlexFloat.from_float(other)
450
+ if not isinstance(other, FlexFloat):
451
+ raise TypeError("Can only multiply FlexFloat instances.")
452
+
453
+ # OBJECTIVE: Multiply two FlexFloat instances together.
454
+ # Based on: https://www.rfwireless-world.com/tutorials/ieee-754-floating-point-arithmetic
455
+ #
456
+ # Steps:
457
+ # 0. Handle special cases (NaN, Infinity, zero).
458
+ # 1. Calculate result sign (XOR of operand signs).
459
+ # 2. Extract and add exponents (subtract bias).
460
+ # 3. Multiply mantissas.
461
+ # 4. Normalize mantissa and adjust exponent if necessary.
462
+ # 5. Check for overflow/underflow.
463
+ # 6. Grow exponent if necessary.
464
+ # 7. Return new FlexFloat instance.
465
+
466
+ # Step 0: Handle special cases
467
+ if self.is_nan() or other.is_nan():
468
+ return FlexFloat.nan()
469
+
470
+ if self.is_zero() or other.is_zero():
471
+ return FlexFloat.zero()
472
+
473
+ if self.is_infinity() or other.is_infinity():
474
+ result_sign = self.sign ^ other.sign
475
+ return FlexFloat.infinity(sign=result_sign)
476
+
477
+ # Step 1: Calculate result sign (XOR of signs)
478
+ result_sign = self.sign ^ other.sign
479
+
480
+ # Step 2: Extract exponent and fraction bits
481
+ # Note: The stored exponent needs +1 to get the actual value (like in addition)
482
+ exponent_self = self.exponent.to_signed_int() + 1
483
+ exponent_other = other.exponent.to_signed_int() + 1
484
+
485
+ # Step 3: Add exponents
486
+ # When multiplying, we add the unbiased exponents
487
+ result_exponent = exponent_self + exponent_other
488
+
489
+ # Step 4: Multiply mantissas
490
+ # Prepend leading 1 to form the mantissa (1.fraction)
491
+ mantissa_self = [True] + self.fraction
492
+ mantissa_other = [True] + other.fraction
493
+
494
+ # Convert mantissas to integers for multiplication
495
+ mantissa_self_int = mantissa_self.to_int()
496
+ mantissa_other_int = mantissa_other.to_int()
497
+
498
+ # Multiply the mantissas
499
+ product = mantissa_self_int * mantissa_other_int
500
+
501
+ # Convert back to bit array
502
+ # The product will have up to 106 bits (53 + 53)
503
+ if product == 0:
504
+ return FlexFloat.zero()
505
+
506
+ product_bits = BitArray.zeros(106)
507
+ for i in range(105, -1, -1):
508
+ product_bits[i] = product & 1 == 1
509
+ product >>= 1
510
+ if product <= 0:
511
+ break
512
+
513
+ # Step 5: Normalize mantissa and adjust exponent if necessary
514
+ # Find the position of the most significant bit
515
+ msb_position = next((i for i, bit in enumerate(product_bits) if bit), None)
516
+
517
+ assert msb_position is not None, "Product should not be zero here."
518
+
519
+ # The mantissa multiplication gives us a result with 2 integer bits
520
+ # We need to normalize to have exactly 1 integer bit
521
+ # If MSB is at position 0, we have a 2-bit integer part (11.xxxxx)
522
+ # If MSB is at position 1, we have a 1-bit integer part (1.xxxxx)
523
+ if msb_position == 0:
524
+ result_exponent += 1
525
+ normalized_mantissa = product_bits[msb_position : msb_position + 53]
526
+
527
+ # Pad with zeros if we don't have enough bits
528
+ missing_bits = 53 - len(normalized_mantissa)
529
+ if missing_bits > 0:
530
+ normalized_mantissa += [False] * missing_bits
531
+
532
+ # Step 6: Grow exponent if necessary to accommodate the result
533
+ exp_result_length = max(len(self.exponent), len(other.exponent))
534
+
535
+ # Check if we need to grow the exponent to accommodate the result
536
+ exp_result_length = self._grow_exponent(result_exponent, exp_result_length)
537
+
538
+ exp_result = BitArray.from_signed_int(result_exponent - 1, exp_result_length)
539
+
540
+ return FlexFloat(
541
+ sign=result_sign,
542
+ exponent=exp_result,
543
+ fraction=normalized_mantissa[1:], # Exclude leading bit
544
+ )
545
+
546
+ def __rmul__(self, other: Number) -> FlexFloat:
547
+ """Right-hand multiplication for Number types.
548
+
549
+ Args:
550
+ other (float | int): The number to multiply with this FlexFloat.
551
+ Returns:
552
+ FlexFloat: A new FlexFloat instance representing the product.
553
+ """
554
+ return self * other
555
+
556
+ def __truediv__(self, other: FlexFloat | Number) -> FlexFloat:
557
+ """Divide this FlexFloat by another FlexFloat or number.
558
+
559
+ Args:
560
+ other (FlexFloat | float | int): The divisor.
561
+ Returns:
562
+ FlexFloat: A new FlexFloat instance representing the quotient.
563
+ """
564
+ if isinstance(other, Number):
565
+ other = FlexFloat.from_float(other)
566
+ if not isinstance(other, FlexFloat):
567
+ raise TypeError("Can only divide FlexFloat instances.")
568
+
569
+ # OBJECTIVE: Divide two FlexFloat instances.
570
+ # Based on: https://www.rfwireless-world.com/tutorials/ieee-754-floating-point-arithmetic
571
+ #
572
+ # Steps:
573
+ # 0. Handle special cases (NaN, Infinity, zero).
574
+ # 1. Calculate result sign (XOR of operand signs).
575
+ # 2. Extract and subtract exponents (add bias).
576
+ # 3. Divide mantissas.
577
+ # 4. Normalize mantissa and adjust exponent if necessary.
578
+ # 5. Check for overflow/underflow.
579
+ # 6. Grow exponent if necessary.
580
+ # 7. Return new FlexFloat instance.
581
+
582
+ # Step 0: Handle special cases
583
+ if self.is_nan() or other.is_nan():
584
+ return FlexFloat.nan()
585
+
586
+ # Zero cases
587
+ if self.is_zero() and other.is_zero():
588
+ return FlexFloat.nan() # 0 / 0 = NaN
589
+ if self.is_zero() and not other.is_zero():
590
+ return FlexFloat.zero() # 0 / finite = 0
591
+ if not self.is_zero() and other.is_zero():
592
+ return FlexFloat.infinity(sign=self.sign ^ other.sign) # finite / 0 = inf
593
+
594
+ # Infinity cases
595
+ if self.is_infinity() and other.is_infinity():
596
+ return FlexFloat.nan() # inf / inf = NaN
597
+ if self.is_infinity():
598
+ return FlexFloat.infinity(sign=self.sign ^ other.sign) # inf / finite = inf
599
+ if other.is_infinity():
600
+ return FlexFloat.zero() # finite / inf = 0
601
+
602
+ # Step 1: Calculate result sign (XOR of signs)
603
+ result_sign = self.sign ^ other.sign
604
+
605
+ # Step 2: Extract exponent and fraction bits
606
+ # Note: The stored exponent needs +1 to get the actual value (like in multiplication)
607
+ exponent_self = self.exponent.to_signed_int() + 1
608
+ exponent_other = other.exponent.to_signed_int() + 1
609
+
610
+ # Step 3: Subtract exponents (for division, we subtract the divisor's exponent)
611
+ result_exponent = exponent_self - exponent_other
612
+
613
+ # Step 4: Divide mantissas
614
+ # Prepend leading 1 to form the mantissa (1.fraction)
615
+ mantissa_self = [True] + self.fraction
616
+ mantissa_other = [True] + other.fraction
617
+
618
+ # Convert mantissas to integers for division
619
+ mantissa_self_int = mantissa_self.to_int()
620
+ mantissa_other_int = mantissa_other.to_int()
621
+
622
+ # Normalize mantissa for division (avoid overflow) -> scale the dividend
623
+ if mantissa_self_int < mantissa_other_int:
624
+ scale_factor = 1 << 53
625
+ result_exponent -= 1 # Adjust exponent since result < 1.0
626
+ else:
627
+ scale_factor = 1 << 52
628
+ scaled_dividend = mantissa_self_int * scale_factor
629
+ quotient = scaled_dividend // mantissa_other_int
630
+
631
+ if quotient == 0:
632
+ return FlexFloat.zero()
633
+
634
+ # Convert quotient to BitArray for easier bit manipulation
635
+ quotient_bitarray = BitArray.zeros(64) # Use a fixed size for consistency
636
+ temp_quotient = quotient
637
+ bit_pos = 63
638
+ while temp_quotient > 0 and bit_pos >= 0:
639
+ quotient_bitarray[bit_pos] = (temp_quotient & 1) == 1
640
+ temp_quotient >>= 1
641
+ bit_pos -= 1
642
+
643
+ # Step 5: Normalize mantissa and adjust exponent if necessary
644
+ # Find the position of the most significant bit (first 1)
645
+ msb_pos = next((i for i, bit in enumerate(quotient_bitarray) if bit), None)
646
+
647
+ if msb_pos is None:
648
+ return FlexFloat.zero()
649
+
650
+ # Extract exactly 53 bits starting from the MSB (1 integer + 52 fraction)
651
+ normalized_mantissa = quotient_bitarray[msb_pos : msb_pos + 53]
652
+ normalized_mantissa = normalized_mantissa.shift(
653
+ 53 - len(normalized_mantissa), fill=False
654
+ )
655
+
656
+ # Step 6: Grow exponent if necessary to accommodate the result
657
+ exp_result_length = max(len(self.exponent), len(other.exponent))
658
+
659
+ # Check if we need to grow the exponent to accommodate the result
660
+ exp_result_length = self._grow_exponent(result_exponent, exp_result_length)
661
+
662
+ exp_result = BitArray.from_signed_int(result_exponent - 1, exp_result_length)
663
+
664
+ return FlexFloat(
665
+ sign=result_sign,
666
+ exponent=exp_result,
667
+ fraction=normalized_mantissa[1:], # Exclude leading bit
668
+ )
669
+
670
+ def __rtruediv__(self, other: Number) -> FlexFloat:
671
+ """Right-hand division for Number types.
672
+
673
+ Args:
674
+ other (float | int): The number to divide by this FlexFloat.
675
+ Returns:
676
+ FlexFloat: A new FlexFloat instance representing the quotient.
677
+ """
678
+ return FlexFloat.from_float(other) / self
flexfloat/py.typed ADDED
File without changes
flexfloat/types.py ADDED
@@ -0,0 +1,5 @@
1
+ """Type definitions for the flexfloat package."""
2
+
3
+ from typing import TypeAlias
4
+
5
+ Number: TypeAlias = int | float
@@ -0,0 +1,82 @@
1
+ Metadata-Version: 2.4
2
+ Name: flexfloat
3
+ Version: 0.1.0
4
+ Summary: A library for arbitrary precision floating point arithmetic
5
+ Author: Ferran Sanchez Llado
6
+ License: MIT
7
+ Classifier: Development Status :: 3 - Alpha
8
+ Classifier: Intended Audience :: Developers
9
+ Classifier: Intended Audience :: Science/Research
10
+ Classifier: License :: OSI Approved :: MIT License
11
+ Classifier: Operating System :: OS Independent
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Programming Language :: Python :: 3.11
14
+ Classifier: Programming Language :: Python :: 3.12
15
+ Classifier: Topic :: Scientific/Engineering :: Mathematics
16
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
17
+ Requires-Python: >=3.11
18
+ Description-Content-Type: text/markdown
19
+ Provides-Extra: dev
20
+ Requires-Dist: pytest>=7.0; extra == "dev"
21
+ Requires-Dist: pytest-cov>=4.0; extra == "dev"
22
+ Requires-Dist: black>=23.0; extra == "dev"
23
+ Requires-Dist: isort>=5.0; extra == "dev"
24
+ Requires-Dist: mypy>=1.0; extra == "dev"
25
+ Requires-Dist: pylint>=3.0; extra == "dev"
26
+ Requires-Dist: flake8>=6.0; extra == "dev"
27
+
28
+ # FlexFloat
29
+
30
+ A Python library for arbitrary precision floating point arithmetic with a flexible exponent and fixed-size fraction.
31
+
32
+ ## Features
33
+
34
+ - **Growable Exponents**: Handle very large or very small numbers by dynamically adjusting the exponent size
35
+ - **Fixed-Size Fractions**: Maintain precision consistency with IEEE 754-compatible 52-bit fractions
36
+ - **IEEE 754 Compatibility**: Follows IEEE 754 double-precision format as the baseline
37
+ - **Special Value Support**: Handles NaN, positive/negative infinity, and zero values
38
+ - **Arithmetic Operations**: Addition and subtraction with proper overflow/underflow handling
39
+
40
+ ## Installation
41
+
42
+ ```bash
43
+ pip install flexfloat
44
+ ```
45
+
46
+ ## Development Installation
47
+
48
+ ```bash
49
+ pip install -e ".[dev]"
50
+ ```
51
+
52
+ ## Usage
53
+
54
+ ```python
55
+ from flexfloat import FlexFloat
56
+
57
+ # Create FlexFloat instances
58
+ a = FlexFloat.from_float(1.5)
59
+ b = FlexFloat.from_float(2.5)
60
+
61
+ # Perform arithmetic operations
62
+ result = a + b
63
+ print(result.to_float()) # 4.0
64
+
65
+ # Handle very large numbers
66
+ large_a = FlexFloat.from_float(1e308)
67
+ large_b = FlexFloat.from_float(1e308)
68
+ large_result = large_a + large_b
69
+ # Result has grown exponent to handle overflow
70
+ print(len(large_result.exponent)) # > 11 (grows beyond IEEE 754 standard)
71
+ ```
72
+
73
+ ## Running Tests
74
+
75
+ ```bash
76
+ python -m pytest tests
77
+ ```
78
+
79
+
80
+ ## License
81
+
82
+ MIT License
@@ -0,0 +1,9 @@
1
+ flexfloat/__init__.py,sha256=fvimTZm80CHm8bp11bID5twYoISERcatgx21snQxsz8,378
2
+ flexfloat/bitarray.py,sha256=Ss0bZUUpfsBPJkPaGymXd2O-4e-UCanUva_encnc4MI,9172
3
+ flexfloat/core.py,sha256=KK8LfRlqReZuVgyN-fQwYDsM1TlaJtYjHcafPYSPZeQ,27570
4
+ flexfloat/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
5
+ flexfloat/types.py,sha256=SFTYmG3BHLiCJc0LvNwOnByRLRcRXH2erINeqLyHLKs,118
6
+ flexfloat-0.1.0.dist-info/METADATA,sha256=8QUYOP_vFgljckHUm02jixPFfDYkt-XZdW5Wahm8TbQ,2459
7
+ flexfloat-0.1.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
8
+ flexfloat-0.1.0.dist-info/top_level.txt,sha256=82S8dY2UoNZh-9pwg7tUvbwB3uw2s3mfEoyUW6vCMdU,10
9
+ flexfloat-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (80.9.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1 @@
1
+ flexfloat