|
25 | 25 | ## Reference-level explanation
|
26 | 26 | [reference-level-explanation]: #reference-level-explanation
|
27 | 27 |
|
28 |
| -This RFC proposes a library addition `amaranth.lib.fixedpoint` with the following contents: |
| 28 | +This RFC proposes a library addition `amaranth.lib.fixed` with the following contents: |
29 | 29 |
|
30 |
| -`FixedPoint` is a `ShapeCastable` subclass. |
| 30 | +`fixed.Shape` is a `ShapeCastable` subclass. |
31 | 31 | The following operations are defined on it:
|
32 | 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. |
| 33 | +- `fixed.Shape(f_width, /, *, signed)`: Create a `fixed.Shape` with zero integer bits and `f_width` fractional bits. |
| 34 | +- `fixed.Shape(i_width, f_width, /, *, signed)`: Create a `fixed.Shape` with `i_width` integer bits and `f_width` fractional bits. |
| 35 | + - The sign bit is not included in `i_width` or `f_width`, so a `fixed.Shape(7, 8, signed=True)` will be 16 bits wide. |
| 36 | +- `fixed.Shape.cast(shape, f_width=0)`: Cast `shape` to a `fixed.Shape` instance. |
36 | 37 | - `.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 | +- `.const(value)`: Create a `fixed.Const` from `value`. |
38 | 39 | - `.as_shape()`: Return the underlying `Shape`.
|
39 |
| -- `.__call__(target)`: Create a `FixedPointValue` over `target`. |
| 40 | +- `.__call__(target)`: Create a `fixed.Value` over `target`. |
40 | 41 |
|
41 |
| -`Q(*args)` is an alias for `FixedPoint(*args, signed=True)`. |
| 42 | +`SQ(*args)` is an alias for `fixed.Shape(*args, signed=True)`. |
42 | 43 |
|
43 |
| -`UQ(*args)` is an alias for `FixedPoint(*args, signed=False)`. |
| 44 | +`UQ(*args)` is an alias for `fixed.Shape(*args, signed=False)`. |
44 | 45 |
|
45 |
| -`FixedPointValue` is a `ValueCastable` subclass. |
| 46 | +`fixed.Value` is a `ValueCastable` subclass. |
46 | 47 | The following operations are defined on it:
|
47 | 48 |
|
48 |
| -- `FixedPointValue(shape, target)`: Create a `FixedPointValue` with `shape` over `target`. |
49 |
| -- `FixedPointValue.cast(value)`: Cast `value` to a `FixedPointValue`. |
| 49 | +- `fixed.Value(shape, target)`: Create a `fixed.Value` with `shape` over `target`. |
| 50 | +- `fixed.Value.cast(value, f_width=0)`: Cast `value` to a `fixed.Value`. |
50 | 51 | - `.i_width`, `.f_width`, `.signed`: Width and signedness properties.
|
51 |
| -- `.shape()`: Return the `FixedPoint` this was created from. |
| 52 | +- `.shape()`: Return the `fixed.Shape` this was created from. |
52 | 53 | - `.as_value()`: Return the underlying value.
|
53 | 54 | - `.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 | 55 | - 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. |
| 56 | + - If `value` is an `int` or `float`, it'll be cast to a `fixed.Const` first. |
| 57 | + - If `value` is a `fixed.Value`, the precision will be extended or rounded as required. |
| 58 | +- `.round(f_width=0)`: Return a new `fixed.Value` with precision changed to `f_width`, rounding as required. |
58 | 59 | - `.__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 `Value`, it'll be cast to a `fixed.Value` first. |
| 61 | + - If `other` is an `int`, it'll be cast to a `fixed.Const` first. |
60 | 62 | - 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. |
| 63 | + - The result will be a new `fixed.Value` with enough precision to hold any resulting value without rounding or overflowing. |
| 64 | +- `.__lshift__(other)`, `.__rshift__(other)`: Bit shift operations. |
62 | 65 | - `.__neg__()`, `.__pos__()`, `.__abs__()`: Unary arithmetic operations.
|
63 | 66 |
|
| 67 | +`fixed.Const` is a `fixed.Value` subclass. |
| 68 | +The following additional operations are defined on it: |
| 69 | + |
| 70 | +- `fixed.Const(value, shape=None)`: Create a `fixed.Const` from `value`. `shape` must be a `fixed.Shape` if specified. |
| 71 | + - If `value` is an `int` and `shape` is not specified, the smallest shape that will fit `value` will be selected. |
| 72 | + - If `value` is a `float` and `shape` is not specified, the smallest shape that gives a perfect representation will be selected. |
| 73 | + If `shape` is specified, `value` will be rounded to the closest representable value first. |
| 74 | +- `.as_integer_ratio()`: Return the value represented as an integer ratio `tuple`. |
| 75 | +- `.as_float()`: Return the value represented as a `float`. |
| 76 | +- Operators are extended to return a `fixed.Const` if all operands are constant. |
| 77 | + |
64 | 78 | ## Drawbacks
|
65 | 79 | [drawbacks]: #drawbacks
|
66 | 80 |
|
|
69 | 83 | ## Rationale and alternatives
|
70 | 84 | [rationale-and-alternatives]: #rationale-and-alternatives
|
71 | 85 |
|
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. |
| 86 | +- `fixed.Value.eq()` could cast a `Value` to a `fixed.Value` first, and thereby shift the value to the integer part instead of assigning directly to the underlying value. |
| 87 | + However, `Value.eq()` would always grab the underlying value of a `fixed.Value`, and for consistency both `.eq()` operations should behave in the same manner. |
74 | 88 | - 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 | 89 | 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 | 90 |
|
77 |
| -- Unlike `.eq()`, it makes sense for arithmetic operations to cast a `Value` to `FixedPointValue`. |
| 91 | +- Unlike `.eq()`, it makes sense for arithmetic operations to cast a `Value` to `fixed.Value`. |
78 | 92 | 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 | 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 | + Not having the sign bit included seems more common, and has the advantage that a number has the same fractional precision whether `i_width` is 0 or not. |
| 96 | + |
| 97 | +- While Q notation names the signed type `Q`, it's more consistent for Amaranth to use `SQ` since other Amaranth types defaults to unsigned. |
| 98 | + |
80 | 99 | ## Prior art
|
81 | 100 | [prior-art]: #prior-art
|
82 | 101 |
|
83 |
| -[Q notation](https://en.wikipedia.org/wiki/Q_(number_format)) is a common and convenient way to specify floating point types. |
| 102 | +[Q notation](https://en.wikipedia.org/wiki/Q_(number_format)) is a common and convenient way to specify fixed point types. |
84 | 103 |
|
85 | 104 | ## Unresolved questions
|
86 | 105 | [unresolved-questions]: #unresolved-questions
|
|
91 | 110 | - We could use the same width for `other` as for `self`, adjusted to the appropriate exponent for the value.
|
92 | 111 | - We could outright reject it, requiring the user to explicitly specify precision like e.g. `value * Q(15).const(1 / 3)`.
|
93 | 112 |
|
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 | 113 | - Are there any other operations that would be good to have?
|
98 | 114 |
|
99 |
| -- Bikeshed all the names. |
| 115 | +- Are there any operations that would be good to *not* have? |
| 116 | + - This API (`fixed.Shape.cast()`) seems confusing and difficult to use. Should we expose it at all? (@whitequark) |
| 117 | + |
| 118 | +- `Decimal` and/or `Fraction` support? |
| 119 | + - This could make sense to have, but both can represent values that's not representable as binary fixed point. |
| 120 | + On the other hand, a Python `float` can perfectly represent any fixed point value up to a total width of 53 bits and any `float` value is perfectly representable as fixed point. |
| 121 | + |
| 122 | +- Name all the things. |
| 123 | + - Library name: |
| 124 | + - Bikeshed: `lib.fixed`, `lib.fixnum`. (@whitequark) |
| 125 | + - Type names: |
| 126 | + - `fixed.Shape` and `fixed.Value` are one option, though I can see why others may object to it. (@whitequark) |
| 127 | + - I feel like the `i_width` and `f_width` names are difficult enough to read that it's of more importance than bikeshedding to come up with something more readable. (@whitequark) |
| 128 | + - `.int_bits`, `.frac_bits`? |
| 129 | + - cursed option: `int, frac = x.width`? |
100 | 130 |
|
101 | 131 | ## Future possibilities
|
102 | 132 | [future-possibilities]: #future-possibilities
|
|
0 commit comments