|
| 1 | +- Start Date: (fill me in with today's date, YYYY-MM-DD) |
| 2 | +- RFC PR: [amaranth-lang/rfcs#0000](https://github.com/amaranth-lang/rfcs/pull/0000) |
| 3 | +- Amaranth Issue: [amaranth-lang/amaranth#0000](https://github.com/amaranth-lang/amaranth/issues/0000) |
| 4 | + |
| 5 | +# Fixed point types |
| 6 | + |
| 7 | +## Summary |
| 8 | +[summary]: #summary |
| 9 | + |
| 10 | +Add fixed point types to Amaranth. |
| 11 | + |
| 12 | +## Motivation |
| 13 | +[motivation]: #motivation |
| 14 | + |
| 15 | +Fractional values in hardware are usually represented as some form of fixed point value. |
| 16 | +Without a first class fixed point type, the user has to manually manage how the value needs to be shifted to be represented as an integer and keep track of how that interacts with arithmetic operations. |
| 17 | + |
| 18 | +A fixed point type would encode and keep track of the precision through arithmetic operations, as well as provide standard operations for converting values to and from fixed point representations with correct rounding. |
| 19 | + |
| 20 | +## Guide-level explanation |
| 21 | +[guide-level-explanation]: #guide-level-explanation |
| 22 | + |
| 23 | +TODO |
| 24 | + |
| 25 | +## Reference-level explanation |
| 26 | +[reference-level-explanation]: #reference-level-explanation |
| 27 | + |
| 28 | +This RFC proposes a library addition `amaranth.lib.fixedpoint` with the following contents: |
| 29 | + |
| 30 | +`FixedPoint` is a `ShapeCastable` subclass. |
| 31 | +The following operations are defined on it: |
| 32 | + |
| 33 | +- `FixedPoint(f_width, /, *, signed)`: Create a `FixedPoint` with `f_width` fractional bits. |
| 34 | +- `FixedPoint(i_width, f_width, /, *, signed)`: Create a `FixedPoint` with `i_width` integer bits and `f_width` fractional bits. |
| 35 | +- `FixedPoint.cast(shape)`: Cast `shape` to a `FixedPoint` instance. |
| 36 | +- `.i_width`, `.f_width`, `.signed`: Width and signedness properties. |
| 37 | +- `.const(value)`: Create a fixed point constant from an `int` or `float`, rounded to the closest representable value. |
| 38 | +- `.as_shape()`: Return the underlying `Shape`. |
| 39 | +- `.__call__(target)`: Create a `FixedPointValue` over `target`. |
| 40 | + |
| 41 | +`Q(*args)` is an alias for `FixedPoint(*args, signed=True)`. |
| 42 | + |
| 43 | +`UQ(*args)` is an alias for `FixedPoint(*args, signed=False)`. |
| 44 | + |
| 45 | +`FixedPointValue` is a `ValueCastable` subclass. |
| 46 | +The following operations are defined on it: |
| 47 | + |
| 48 | +- `FixedPointValue(shape, target)`: Create a `FixedPointValue` with `shape` over `target`. |
| 49 | +- `FixedPointValue.cast(value)`: Cast `value` to a `FixedPointValue`. |
| 50 | +- `.i_width`, `.f_width`, `.signed`: Width and signedness properties. |
| 51 | +- `.shape()`: Return the `FixedPoint` this was created from. |
| 52 | +- `.as_value()`: Return the underlying value. |
| 53 | +- `.eq(value)`: Assign `value`. |
| 54 | + - If `value` is a `FixedPointValue`, the precision will be extended or rounded as required. |
| 55 | + - If `value` is an `int` or `float`, the value will be rounded to the closest representable value. |
| 56 | + - If `value` is a `Value`, it'll be assigned directly to the underlying `Value`. |
| 57 | +- `.round(f_width=0)`: Return a new `FixedPointValue` with precision changed to `f_width`, rounding as required. |
| 58 | +- `.__add__(other)`, `.__radd__(other)`, `.__sub__(other)`, `.__rsub__(other)`, `.__mul__(other)`, `.__rmul__(other)`: Binary arithmetic operations. |
| 59 | + - If `other` is a `Value` or an `int`, it'll be cast to a `FixedPointValue` first. |
| 60 | + - If `other` is a `float`: TBD |
| 61 | + - The result will be a new `FixedPointValue` with enough precision to hold any resulting value without rounding or overflowing. |
| 62 | +- `.__neg__()`, `.__pos__()`, `.__abs__()`: Unary arithmetic operations. |
| 63 | + |
| 64 | +## Drawbacks |
| 65 | +[drawbacks]: #drawbacks |
| 66 | + |
| 67 | +TBD |
| 68 | + |
| 69 | +## Rationale and alternatives |
| 70 | +[rationale-and-alternatives]: #rationale-and-alternatives |
| 71 | + |
| 72 | +- `FixedPointValue.eq()` could cast a `Value` to a `FixedPointValue` first, and thereby shift the value to the integer part instead of assigning directly to the underlying value. |
| 73 | + However, `Value.eq()` would always grab the underlying value of a `FixedPointValue`, and for consistency both `.eq()` operations should behave in the same manner. |
| 74 | + - If we wanted to go the other way, this RFC could be deferred until another RFC allowing `ValueCastable` to override reflected `.eq()` have been merged. |
| 75 | + However, having to explicitly do `value.eq(fp_value.round())` when rounding is desired is arguably preferable to having `value.eq(fp_value)` do an implicit rounding. |
| 76 | + |
| 77 | +- Unlike `.eq()`, it makes sense for arithmetic operations to cast a `Value` to `FixedPointValue`. |
| 78 | + Multiplying an integer with a fixedpoint constant and rounding the result back to an integer is a reasonable and likely common thing to want to do. |
| 79 | + |
| 80 | +## Prior art |
| 81 | +[prior-art]: #prior-art |
| 82 | + |
| 83 | +[Q notation](https://en.wikipedia.org/wiki/Q_(number_format)) is a common and convenient way to specify floating point types. |
| 84 | + |
| 85 | +## Unresolved questions |
| 86 | +[unresolved-questions]: #unresolved-questions |
| 87 | + |
| 88 | +- What should we do if a `float` is passed as `other` to an arithmetic operation? |
| 89 | + - We could use `float.as_integer_ratio()` to derive a perfect fixed point representation. |
| 90 | + However, since a Python `float` is double precision, this means it's easy to make a >50 bit number by accident by doing something like `value * (1 / 3)`, and even if the result is rounded or truncated afterwards, the lower bits can affect rounding and thus won't be optimized out in synthesis. |
| 91 | + - We could use the same width for `other` as for `self`, adjusted to the appropriate exponent for the value. |
| 92 | + - We could outright reject it, requiring the user to explicitly specify precision like e.g. `value * Q(15).const(1 / 3)`. |
| 93 | + |
| 94 | +- There's two slightly different [Q notation](https://en.wikipedia.org/wiki/Q_(number_format)) definitions, namely whether the bit counts includes the sign bit or not. |
| 95 | + `UQ(15)` and `UQ(7, 8)` would be 15 bits in either convention, but `Q(15)` and `Q(7, 8)` would be 15 or 16 bits depending on the convention. Which do we pick? |
| 96 | + |
| 97 | +- Are there any other operations that would be good to have? |
| 98 | + |
| 99 | +- Bikeshed all the names. |
| 100 | + |
| 101 | +## Future possibilities |
| 102 | +[future-possibilities]: #future-possibilities |
| 103 | + |
| 104 | +TBD |
0 commit comments