From 51ae0d7ee6e830f0c11dacc182e63eae070657e0 Mon Sep 17 00:00:00 2001 From: Vegard Storheil Eriksen Date: Sat, 23 Dec 2023 00:04:52 +0100 Subject: [PATCH 01/13] Add an RFC for fixed point types. --- text/0041-fixed-point.md | 104 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 104 insertions(+) create mode 100644 text/0041-fixed-point.md diff --git a/text/0041-fixed-point.md b/text/0041-fixed-point.md new file mode 100644 index 0000000..e34504f --- /dev/null +++ b/text/0041-fixed-point.md @@ -0,0 +1,104 @@ +- Start Date: (fill me in with today's date, YYYY-MM-DD) +- RFC PR: [amaranth-lang/rfcs#41](https://github.com/amaranth-lang/rfcs/pull/41) +- Amaranth Issue: [amaranth-lang/amaranth#0000](https://github.com/amaranth-lang/amaranth/issues/0000) + +# Fixed point types + +## Summary +[summary]: #summary + +Add fixed point types to Amaranth. + +## Motivation +[motivation]: #motivation + +Fractional values in hardware are usually represented as some form of fixed point value. +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. + +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. + +## Guide-level explanation +[guide-level-explanation]: #guide-level-explanation + +TODO + +## Reference-level explanation +[reference-level-explanation]: #reference-level-explanation + +This RFC proposes a library addition `amaranth.lib.fixedpoint` with the following contents: + +`FixedPoint` is a `ShapeCastable` subclass. +The following operations are defined on it: + +- `FixedPoint(f_width, /, *, signed)`: Create a `FixedPoint` with `f_width` fractional bits. +- `FixedPoint(i_width, f_width, /, *, signed)`: Create a `FixedPoint` with `i_width` integer bits and `f_width` fractional bits. +- `FixedPoint.cast(shape)`: Cast `shape` to a `FixedPoint` instance. +- `.i_width`, `.f_width`, `.signed`: Width and signedness properties. +- `.const(value)`: Create a fixed point constant from an `int` or `float`, rounded to the closest representable value. +- `.as_shape()`: Return the underlying `Shape`. +- `.__call__(target)`: Create a `FixedPointValue` over `target`. + +`Q(*args)` is an alias for `FixedPoint(*args, signed=True)`. + +`UQ(*args)` is an alias for `FixedPoint(*args, signed=False)`. + +`FixedPointValue` is a `ValueCastable` subclass. +The following operations are defined on it: + +- `FixedPointValue(shape, target)`: Create a `FixedPointValue` with `shape` over `target`. +- `FixedPointValue.cast(value)`: Cast `value` to a `FixedPointValue`. +- `.i_width`, `.f_width`, `.signed`: Width and signedness properties. +- `.shape()`: Return the `FixedPoint` this was created from. +- `.as_value()`: Return the underlying value. +- `.eq(value)`: Assign `value`. + - If `value` is a `FixedPointValue`, the precision will be extended or rounded as required. + - If `value` is an `int` or `float`, the value will be rounded to the closest representable value. + - If `value` is a `Value`, it'll be assigned directly to the underlying `Value`. +- `.round(f_width=0)`: Return a new `FixedPointValue` with precision changed to `f_width`, rounding as required. +- `.__add__(other)`, `.__radd__(other)`, `.__sub__(other)`, `.__rsub__(other)`, `.__mul__(other)`, `.__rmul__(other)`: Binary arithmetic operations. + - If `other` is a `Value` or an `int`, it'll be cast to a `FixedPointValue` first. + - If `other` is a `float`: TBD + - The result will be a new `FixedPointValue` with enough precision to hold any resulting value without rounding or overflowing. +- `.__neg__()`, `.__pos__()`, `.__abs__()`: Unary arithmetic operations. + +## Drawbacks +[drawbacks]: #drawbacks + +TBD + +## Rationale and alternatives +[rationale-and-alternatives]: #rationale-and-alternatives + +- `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. + However, `Value.eq()` would always grab the underlying value of a `FixedPointValue`, and for consistency both `.eq()` operations should behave in the same manner. + - 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. + 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. + +- Unlike `.eq()`, it makes sense for arithmetic operations to cast a `Value` to `FixedPointValue`. + 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. + +## Prior art +[prior-art]: #prior-art + +[Q notation](https://en.wikipedia.org/wiki/Q_(number_format)) is a common and convenient way to specify floating point types. + +## Unresolved questions +[unresolved-questions]: #unresolved-questions + +- What should we do if a `float` is passed as `other` to an arithmetic operation? + - We could use `float.as_integer_ratio()` to derive a perfect fixed point representation. + 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. + - We could use the same width for `other` as for `self`, adjusted to the appropriate exponent for the value. + - We could outright reject it, requiring the user to explicitly specify precision like e.g. `value * Q(15).const(1 / 3)`. + +- 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. + `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? + +- Are there any other operations that would be good to have? + +- Bikeshed all the names. + +## Future possibilities +[future-possibilities]: #future-possibilities + +TBD From 8abb235e900e0d089094578beeb73789c7cfaede Mon Sep 17 00:00:00 2001 From: Vegard Storheil Eriksen Date: Sat, 23 Dec 2023 21:35:11 +0100 Subject: [PATCH 02/13] Updated to address feedback. --- text/0041-fixed-point.md | 82 +++++++++++++++++++++++++++------------- 1 file changed, 56 insertions(+), 26 deletions(-) diff --git a/text/0041-fixed-point.md b/text/0041-fixed-point.md index e34504f..e75a333 100644 --- a/text/0041-fixed-point.md +++ b/text/0041-fixed-point.md @@ -25,42 +25,56 @@ TODO ## Reference-level explanation [reference-level-explanation]: #reference-level-explanation -This RFC proposes a library addition `amaranth.lib.fixedpoint` with the following contents: +This RFC proposes a library addition `amaranth.lib.fixed` with the following contents: -`FixedPoint` is a `ShapeCastable` subclass. +`fixed.Shape` is a `ShapeCastable` subclass. The following operations are defined on it: -- `FixedPoint(f_width, /, *, signed)`: Create a `FixedPoint` with `f_width` fractional bits. -- `FixedPoint(i_width, f_width, /, *, signed)`: Create a `FixedPoint` with `i_width` integer bits and `f_width` fractional bits. -- `FixedPoint.cast(shape)`: Cast `shape` to a `FixedPoint` instance. +- `fixed.Shape(f_width, /, *, signed)`: Create a `fixed.Shape` with zero integer bits and `f_width` fractional bits. +- `fixed.Shape(i_width, f_width, /, *, signed)`: Create a `fixed.Shape` with `i_width` integer bits and `f_width` fractional bits. + - 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. +- `fixed.Shape.cast(shape, f_width=0)`: Cast `shape` to a `fixed.Shape` instance. - `.i_width`, `.f_width`, `.signed`: Width and signedness properties. -- `.const(value)`: Create a fixed point constant from an `int` or `float`, rounded to the closest representable value. +- `.const(value)`: Create a `fixed.Const` from `value`. - `.as_shape()`: Return the underlying `Shape`. -- `.__call__(target)`: Create a `FixedPointValue` over `target`. +- `.__call__(target)`: Create a `fixed.Value` over `target`. -`Q(*args)` is an alias for `FixedPoint(*args, signed=True)`. +`SQ(*args)` is an alias for `fixed.Shape(*args, signed=True)`. -`UQ(*args)` is an alias for `FixedPoint(*args, signed=False)`. +`UQ(*args)` is an alias for `fixed.Shape(*args, signed=False)`. -`FixedPointValue` is a `ValueCastable` subclass. +`fixed.Value` is a `ValueCastable` subclass. The following operations are defined on it: -- `FixedPointValue(shape, target)`: Create a `FixedPointValue` with `shape` over `target`. -- `FixedPointValue.cast(value)`: Cast `value` to a `FixedPointValue`. +- `fixed.Value(shape, target)`: Create a `fixed.Value` with `shape` over `target`. +- `fixed.Value.cast(value, f_width=0)`: Cast `value` to a `fixed.Value`. - `.i_width`, `.f_width`, `.signed`: Width and signedness properties. -- `.shape()`: Return the `FixedPoint` this was created from. +- `.shape()`: Return the `fixed.Shape` this was created from. - `.as_value()`: Return the underlying value. - `.eq(value)`: Assign `value`. - - If `value` is a `FixedPointValue`, the precision will be extended or rounded as required. - - If `value` is an `int` or `float`, the value will be rounded to the closest representable value. - If `value` is a `Value`, it'll be assigned directly to the underlying `Value`. -- `.round(f_width=0)`: Return a new `FixedPointValue` with precision changed to `f_width`, rounding as required. + - If `value` is an `int` or `float`, it'll be cast to a `fixed.Const` first. + - If `value` is a `fixed.Value`, the precision will be extended or rounded as required. +- `.round(f_width=0)`: Return a new `fixed.Value` with precision changed to `f_width`, rounding as required. - `.__add__(other)`, `.__radd__(other)`, `.__sub__(other)`, `.__rsub__(other)`, `.__mul__(other)`, `.__rmul__(other)`: Binary arithmetic operations. - - If `other` is a `Value` or an `int`, it'll be cast to a `FixedPointValue` first. + - If `other` is a `Value`, it'll be cast to a `fixed.Value` first. + - If `other` is an `int`, it'll be cast to a `fixed.Const` first. - If `other` is a `float`: TBD - - The result will be a new `FixedPointValue` with enough precision to hold any resulting value without rounding or overflowing. + - The result will be a new `fixed.Value` with enough precision to hold any resulting value without rounding or overflowing. +- `.__lshift__(other)`, `.__rshift__(other)`: Bit shift operations. - `.__neg__()`, `.__pos__()`, `.__abs__()`: Unary arithmetic operations. +`fixed.Const` is a `fixed.Value` subclass. +The following additional operations are defined on it: + +- `fixed.Const(value, shape=None)`: Create a `fixed.Const` from `value`. `shape` must be a `fixed.Shape` if specified. + - If `value` is an `int` and `shape` is not specified, the smallest shape that will fit `value` will be selected. + - If `value` is a `float` and `shape` is not specified, the smallest shape that gives a perfect representation will be selected. + If `shape` is specified, `value` will be rounded to the closest representable value first. +- `.as_integer_ratio()`: Return the value represented as an integer ratio `tuple`. +- `.as_float()`: Return the value represented as a `float`. +- Operators are extended to return a `fixed.Const` if all operands are constant. + ## Drawbacks [drawbacks]: #drawbacks @@ -69,18 +83,23 @@ TBD ## Rationale and alternatives [rationale-and-alternatives]: #rationale-and-alternatives -- `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. - However, `Value.eq()` would always grab the underlying value of a `FixedPointValue`, and for consistency both `.eq()` operations should behave in the same manner. +- `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. + 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. - 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. 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. -- Unlike `.eq()`, it makes sense for arithmetic operations to cast a `Value` to `FixedPointValue`. +- Unlike `.eq()`, it makes sense for arithmetic operations to cast a `Value` to `fixed.Value`. 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. +- 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. + 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. + +- While Q notation names the signed type `Q`, it's more consistent for Amaranth to use `SQ` since other Amaranth types defaults to unsigned. + ## Prior art [prior-art]: #prior-art -[Q notation](https://en.wikipedia.org/wiki/Q_(number_format)) is a common and convenient way to specify floating point types. +[Q notation](https://en.wikipedia.org/wiki/Q_(number_format)) is a common and convenient way to specify fixed point types. ## Unresolved questions [unresolved-questions]: #unresolved-questions @@ -91,12 +110,23 @@ TBD - We could use the same width for `other` as for `self`, adjusted to the appropriate exponent for the value. - We could outright reject it, requiring the user to explicitly specify precision like e.g. `value * Q(15).const(1 / 3)`. -- 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. - `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? - - Are there any other operations that would be good to have? -- Bikeshed all the names. +- Are there any operations that would be good to *not* have? + - This API (`fixed.Shape.cast()`) seems confusing and difficult to use. Should we expose it at all? (@whitequark) + +- `Decimal` and/or `Fraction` support? + - This could make sense to have, but both can represent values that's not representable as binary fixed point. + 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. + +- Name all the things. + - Library name: + - Bikeshed: `lib.fixed`, `lib.fixnum`. (@whitequark) + - Type names: + - `fixed.Shape` and `fixed.Value` are one option, though I can see why others may object to it. (@whitequark) + - 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) + - `.int_bits`, `.frac_bits`? + - cursed option: `int, frac = x.width`? ## Future possibilities [future-possibilities]: #future-possibilities From 9df1ab83f7d798dc92cf96c6ca4fe8c8c61ca3fe Mon Sep 17 00:00:00 2001 From: Vegard Storheil Eriksen Date: Sat, 30 Dec 2023 12:51:26 +0100 Subject: [PATCH 03/13] Added comparison operators and a discussion of rounding. --- text/0041-fixed-point.md | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/text/0041-fixed-point.md b/text/0041-fixed-point.md index e75a333..810281d 100644 --- a/text/0041-fixed-point.md +++ b/text/0041-fixed-point.md @@ -56,13 +56,14 @@ The following operations are defined on it: - If `value` is an `int` or `float`, it'll be cast to a `fixed.Const` first. - If `value` is a `fixed.Value`, the precision will be extended or rounded as required. - `.round(f_width=0)`: Return a new `fixed.Value` with precision changed to `f_width`, rounding as required. -- `.__add__(other)`, `.__radd__(other)`, `.__sub__(other)`, `.__rsub__(other)`, `.__mul__(other)`, `.__rmul__(other)`: Binary arithmetic operations. +- `.__add__(other)`, `.__radd__(other)`, `.__sub__(other)`, `.__rsub__(other)`, `.__mul__(other)`, `.__rmul__(other)`: Binary arithmetic operators. - If `other` is a `Value`, it'll be cast to a `fixed.Value` first. - If `other` is an `int`, it'll be cast to a `fixed.Const` first. - If `other` is a `float`: TBD - The result will be a new `fixed.Value` with enough precision to hold any resulting value without rounding or overflowing. -- `.__lshift__(other)`, `.__rshift__(other)`: Bit shift operations. -- `.__neg__()`, `.__pos__()`, `.__abs__()`: Unary arithmetic operations. +- `.__lshift__(other)`, `.__rshift__(other)`: Bit shift operators. +- `.__neg__()`, `.__pos__()`, `.__abs__()`: Unary arithmetic operators. +- `.__lt__(other)`, `.__le__(other)`, `.__eq__(other)`, `.__ne__(other)`, `.__gt__(other)`, `.__ge__(other)`: Comparison operators. `fixed.Const` is a `fixed.Value` subclass. The following additional operations are defined on it: @@ -110,6 +111,13 @@ TBD - We could use the same width for `other` as for `self`, adjusted to the appropriate exponent for the value. - We could outright reject it, requiring the user to explicitly specify precision like e.g. `value * Q(15).const(1 / 3)`. +- How should we handle rounding? + - Truncating and adding the most significant truncated bit is cheap and is effectively round to nearest with ties rounded towards positive infinity. + - Simply truncating is free, rounds towards negative infinity. + - IEEE 754 defaults to round to nearest, ties to even, which is more expensive to implement. + - Should we make it user selectable? + - We still need a default mode used when a higher precision number is passed to `.eq()`. + - Are there any other operations that would be good to have? - Are there any operations that would be good to *not* have? @@ -127,6 +135,7 @@ TBD - 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) - `.int_bits`, `.frac_bits`? - cursed option: `int, frac = x.width`? + - `.round()` is a bit awkwardly named when it's used both to increase and decrease precision. ## Future possibilities [future-possibilities]: #future-possibilities From 180c1d489595cb7b1b92eabc6beb3c8af104eb69 Mon Sep 17 00:00:00 2001 From: Sebastian Holzapfel Date: Sat, 23 Nov 2024 16:10:29 +0100 Subject: [PATCH 04/13] fixed.Shape: modify signature to accept underlying storage and `f_bits` --- text/0041-fixed-point.md | 33 +++++++++++++++++++-------------- 1 file changed, 19 insertions(+), 14 deletions(-) diff --git a/text/0041-fixed-point.md b/text/0041-fixed-point.md index 810281d..ee9f11c 100644 --- a/text/0041-fixed-point.md +++ b/text/0041-fixed-point.md @@ -27,35 +27,38 @@ TODO This RFC proposes a library addition `amaranth.lib.fixed` with the following contents: +### `fixed.Shape` + `fixed.Shape` is a `ShapeCastable` subclass. The following operations are defined on it: -- `fixed.Shape(f_width, /, *, signed)`: Create a `fixed.Shape` with zero integer bits and `f_width` fractional bits. -- `fixed.Shape(i_width, f_width, /, *, signed)`: Create a `fixed.Shape` with `i_width` integer bits and `f_width` fractional bits. - - 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. -- `fixed.Shape.cast(shape, f_width=0)`: Cast `shape` to a `fixed.Shape` instance. -- `.i_width`, `.f_width`, `.signed`: Width and signedness properties. +- `fixed.Shape(shape, f_bits)`: Create a `fixed.Shape` with underlying storage `shape` and `f_bits` fractional bits. +- The signedness is inherited from `shape`, so a `fixed.Shape(signed(16), 12)` would be a signed fixed-point number, 16 bits wide with 12 fractional bits. +- A `fixed.Shape` may be constructed using the following aliases: + - `SQ(i_bits, f_bits)` is an alias for `fixed.Shape(signed(i_bits + f_bits), f_bits)`. + - `UQ(i_bits, f_bits)` is an alias for `fixed.Shape(unsigned(i_bits + f_bits), f_bits)`. +- `fixed.Shape.cast(shape, f_bits=0)`: Cast `shape` to a `fixed.Shape` instance. +- `.i_bits`, `.f_bits`, `.signed`: Width and signedness properties of the `fixed.Shape`. + - `.i_bits` includes the sign bit. That is, for `fixed.Shape(signed(16), 12)`, `.i_bits == 4`. - `.const(value)`: Create a `fixed.Const` from `value`. - `.as_shape()`: Return the underlying `Shape`. - `.__call__(target)`: Create a `fixed.Value` over `target`. -`SQ(*args)` is an alias for `fixed.Shape(*args, signed=True)`. - -`UQ(*args)` is an alias for `fixed.Shape(*args, signed=False)`. +### `fixed.Value` `fixed.Value` is a `ValueCastable` subclass. The following operations are defined on it: - `fixed.Value(shape, target)`: Create a `fixed.Value` with `shape` over `target`. -- `fixed.Value.cast(value, f_width=0)`: Cast `value` to a `fixed.Value`. -- `.i_width`, `.f_width`, `.signed`: Width and signedness properties. +- `fixed.Value.cast(value, f_bits=0)`: Cast `value` to a `fixed.Value`. +- `.i_bits`, `.f_bits`, `.signed`: Width and signedness properties. - `.shape()`: Return the `fixed.Shape` this was created from. - `.as_value()`: Return the underlying value. - `.eq(value)`: Assign `value`. - If `value` is a `Value`, it'll be assigned directly to the underlying `Value`. - If `value` is an `int` or `float`, it'll be cast to a `fixed.Const` first. - If `value` is a `fixed.Value`, the precision will be extended or rounded as required. -- `.round(f_width=0)`: Return a new `fixed.Value` with precision changed to `f_width`, rounding as required. +- `.round(f_bits=0)`: Return a new `fixed.Value` with precision changed to `f_bits`, rounding as required. - `.__add__(other)`, `.__radd__(other)`, `.__sub__(other)`, `.__rsub__(other)`, `.__mul__(other)`, `.__rmul__(other)`: Binary arithmetic operators. - If `other` is a `Value`, it'll be cast to a `fixed.Value` first. - If `other` is an `int`, it'll be cast to a `fixed.Const` first. @@ -65,6 +68,8 @@ The following operations are defined on it: - `.__neg__()`, `.__pos__()`, `.__abs__()`: Unary arithmetic operators. - `.__lt__(other)`, `.__le__(other)`, `.__eq__(other)`, `.__ne__(other)`, `.__gt__(other)`, `.__ge__(other)`: Comparison operators. +### `fixed.Const` + `fixed.Const` is a `fixed.Value` subclass. The following additional operations are defined on it: @@ -93,9 +98,9 @@ TBD 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. - 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. - 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. - -- While Q notation names the signed type `Q`, it's more consistent for Amaranth to use `SQ` since other Amaranth types defaults to unsigned. + - Not having the sign bit included seems more common, and has the advantage that a number has the same fractional precision whether `i_bits` is 0 or not. + - Having the sign bit included is the dominant notation in the audio ASIC world (citation needed, comment from samimia-swks@). As of now, this RFC uses this notation as it is also a little simpler to reason about the size of underlying storage on constructing an `SQ`. + - While Q notation names the signed type `Q`, it's more consistent for Amaranth to use `SQ` since other Amaranth types defaults to unsigned. ## Prior art [prior-art]: #prior-art From 6d5b91b0f9fcd453dd2a7655cbb9fa59e2986a9c Mon Sep 17 00:00:00 2001 From: Sebastian Holzapfel Date: Sat, 23 Nov 2024 16:17:25 +0100 Subject: [PATCH 05/13] fixed.Shape: add min(), max() methods --- text/0041-fixed-point.md | 1 + 1 file changed, 1 insertion(+) diff --git a/text/0041-fixed-point.md b/text/0041-fixed-point.md index ee9f11c..362885a 100644 --- a/text/0041-fixed-point.md +++ b/text/0041-fixed-point.md @@ -43,6 +43,7 @@ The following operations are defined on it: - `.const(value)`: Create a `fixed.Const` from `value`. - `.as_shape()`: Return the underlying `Shape`. - `.__call__(target)`: Create a `fixed.Value` over `target`. +- `min()`, `max()`: Returns a `fixed.Const` representing the minimum and maximum representable values of a shape. ### `fixed.Value` From cd8c9bc0d5ec38104fa3f1c527706b2f3521ad68 Mon Sep 17 00:00:00 2001 From: Sebastian Holzapfel Date: Sat, 23 Nov 2024 16:23:13 +0100 Subject: [PATCH 06/13] fixed.Const: add `clamp` argument used for interpreting constants --- text/0041-fixed-point.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/text/0041-fixed-point.md b/text/0041-fixed-point.md index 362885a..9075c31 100644 --- a/text/0041-fixed-point.md +++ b/text/0041-fixed-point.md @@ -74,10 +74,12 @@ The following operations are defined on it: `fixed.Const` is a `fixed.Value` subclass. The following additional operations are defined on it: -- `fixed.Const(value, shape=None)`: Create a `fixed.Const` from `value`. `shape` must be a `fixed.Shape` if specified. +- `fixed.Const(value, shape=None, clamp=False)`: Create a `fixed.Const` from `value`. `shape` must be a `fixed.Shape` if specified. - If `value` is an `int` and `shape` is not specified, the smallest shape that will fit `value` will be selected. - If `value` is a `float` and `shape` is not specified, the smallest shape that gives a perfect representation will be selected. If `shape` is specified, `value` will be rounded to the closest representable value first. + - If `shape` is specified and `value` is too large to be represented by that shape, an exception is thrown. + - The exception invites the user to try `clamp=True` to squash this exception, instead clamping the constant to the maximum / minimum value representable by the provided `shape`. - `.as_integer_ratio()`: Return the value represented as an integer ratio `tuple`. - `.as_float()`: Return the value represented as a `float`. - Operators are extended to return a `fixed.Const` if all operands are constant. From b3121b1560225e643e7b0c4d979d2b908c491b5f Mon Sep 17 00:00:00 2001 From: Sebastian Holzapfel Date: Sat, 23 Nov 2024 16:31:13 +0100 Subject: [PATCH 07/13] fixed.Value: add truncate() method, document rounding strategy --- text/0041-fixed-point.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/text/0041-fixed-point.md b/text/0041-fixed-point.md index 9075c31..7e2b027 100644 --- a/text/0041-fixed-point.md +++ b/text/0041-fixed-point.md @@ -60,6 +60,9 @@ The following operations are defined on it: - If `value` is an `int` or `float`, it'll be cast to a `fixed.Const` first. - If `value` is a `fixed.Value`, the precision will be extended or rounded as required. - `.round(f_bits=0)`: Return a new `fixed.Value` with precision changed to `f_bits`, rounding as required. + - Rounding strategy: round to nearest with ties rounded towards positive infinity. + - Under the hood, this involves truncating and adding the most significant truncated bit. +- `.truncate(f_bits=0)`: Return a new `fixed.Value` with precision changed to `f_bits`, truncating as required. - `.__add__(other)`, `.__radd__(other)`, `.__sub__(other)`, `.__rsub__(other)`, `.__mul__(other)`, `.__rmul__(other)`: Binary arithmetic operators. - If `other` is a `Value`, it'll be cast to a `fixed.Value` first. - If `other` is an `int`, it'll be cast to a `fixed.Const` first. From 5eaba6c5ce69b6e6ff99563d2ab92c21a34ead16 Mon Sep 17 00:00:00 2001 From: Sebastian Holzapfel Date: Sat, 23 Nov 2024 16:52:20 +0100 Subject: [PATCH 08/13] fixed.Value: prohibit arithmetic operators on difficult-to-reason-about arguments --- text/0041-fixed-point.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/text/0041-fixed-point.md b/text/0041-fixed-point.md index 7e2b027..bac1191 100644 --- a/text/0041-fixed-point.md +++ b/text/0041-fixed-point.md @@ -69,8 +69,14 @@ The following operations are defined on it: - If `other` is a `float`: TBD - The result will be a new `fixed.Value` with enough precision to hold any resulting value without rounding or overflowing. - `.__lshift__(other)`, `.__rshift__(other)`: Bit shift operators. + - `other` only accepts integral types. For example, shifting by a `float` or `fixed.Value` is not permitted. - `.__neg__()`, `.__pos__()`, `.__abs__()`: Unary arithmetic operators. - `.__lt__(other)`, `.__le__(other)`, `.__eq__(other)`, `.__ne__(other)`, `.__gt__(other)`, `.__ge__(other)`: Comparison operators. + - Comparisons between `fixed.Value` of matching size, or between `fixed.Value` and `int` are permitted. + - Comparisons between `fixed.Value` of different widths are not permitted. + - Users are guided by an exception to explicitly `truncate()` or `round()` as needed. + - Comparisons between `fixed.Value` and `float` are not permitted. + - Users are guided by an exception to explicitly convert using `fixed.Const` as needed. ### `fixed.Const` From 09c21a15fca6c6b378bda53ca7a22e09c44e76a2 Mon Sep 17 00:00:00 2001 From: Sebastian Holzapfel Date: Sat, 23 Nov 2024 17:07:38 +0100 Subject: [PATCH 09/13] fixed.Value: add numerator() method, a signedness-preserving as_value() --- text/0041-fixed-point.md | 1 + 1 file changed, 1 insertion(+) diff --git a/text/0041-fixed-point.md b/text/0041-fixed-point.md index bac1191..e0b805c 100644 --- a/text/0041-fixed-point.md +++ b/text/0041-fixed-point.md @@ -55,6 +55,7 @@ The following operations are defined on it: - `.i_bits`, `.f_bits`, `.signed`: Width and signedness properties. - `.shape()`: Return the `fixed.Shape` this was created from. - `.as_value()`: Return the underlying value. +- `.numerator()`: Return `as_value()` cast to the appropriate signedness. - `.eq(value)`: Assign `value`. - If `value` is a `Value`, it'll be assigned directly to the underlying `Value`. - If `value` is an `int` or `float`, it'll be cast to a `fixed.Const` first. From 630cfde2a4180c4946119101611901e0981bc505 Mon Sep 17 00:00:00 2001 From: Sebastian Holzapfel Date: Sat, 23 Nov 2024 17:44:08 +0100 Subject: [PATCH 10/13] Unresolved: add a proposal to all unresolved questions from original RFC --- text/0041-fixed-point.md | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/text/0041-fixed-point.md b/text/0041-fixed-point.md index e0b805c..5213f84 100644 --- a/text/0041-fixed-point.md +++ b/text/0041-fixed-point.md @@ -37,7 +37,6 @@ The following operations are defined on it: - A `fixed.Shape` may be constructed using the following aliases: - `SQ(i_bits, f_bits)` is an alias for `fixed.Shape(signed(i_bits + f_bits), f_bits)`. - `UQ(i_bits, f_bits)` is an alias for `fixed.Shape(unsigned(i_bits + f_bits), f_bits)`. -- `fixed.Shape.cast(shape, f_bits=0)`: Cast `shape` to a `fixed.Shape` instance. - `.i_bits`, `.f_bits`, `.signed`: Width and signedness properties of the `fixed.Shape`. - `.i_bits` includes the sign bit. That is, for `fixed.Shape(signed(16), 12)`, `.i_bits == 4`. - `.const(value)`: Create a `fixed.Const` from `value`. @@ -67,7 +66,7 @@ The following operations are defined on it: - `.__add__(other)`, `.__radd__(other)`, `.__sub__(other)`, `.__rsub__(other)`, `.__mul__(other)`, `.__rmul__(other)`: Binary arithmetic operators. - If `other` is a `Value`, it'll be cast to a `fixed.Value` first. - If `other` is an `int`, it'll be cast to a `fixed.Const` first. - - If `other` is a `float`: TBD + - If `other` is a `float`, this is not permitted. The `float` must be explicitly cast to `fixed.Const`. - The result will be a new `fixed.Value` with enough precision to hold any resulting value without rounding or overflowing. - `.__lshift__(other)`, `.__rshift__(other)`: Bit shift operators. - `other` only accepts integral types. For example, shifting by a `float` or `fixed.Value` is not permitted. @@ -128,6 +127,7 @@ TBD 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. - We could use the same width for `other` as for `self`, adjusted to the appropriate exponent for the value. - We could outright reject it, requiring the user to explicitly specify precision like e.g. `value * Q(15).const(1 / 3)`. + - vk2seb@: I would lean toward outright rejecting this, with an explicit cast necessary (now reflected above). - How should we handle rounding? - Truncating and adding the most significant truncated bit is cheap and is effectively round to nearest with ties rounded towards positive infinity. @@ -135,15 +135,21 @@ TBD - IEEE 754 defaults to round to nearest, ties to even, which is more expensive to implement. - Should we make it user selectable? - We still need a default mode used when a higher precision number is passed to `.eq()`. + - vk2seb@: Both truncation and simple rounding (round to nearest) are commonly used in DSP algorithms. For now, we provide only `truncate()` and `round()` strategies (now reflected above). Additional rounding strategies may be added in a future RFC, however we will always need a default rounding strategy. - Are there any other operations that would be good to have? + - From ld-cd@: `min()`, `max()` on `fixed.Shape` (vk2seb@: agree, heavily use this) + - From ld-cd@: `numerator()` as a signedness-preserving `as_value()` (vk2seb@: agree, heavily use this) + - vk2seb@: Let's add both of these operations (now reflected above) - Are there any operations that would be good to *not* have? - This API (`fixed.Shape.cast()`) seems confusing and difficult to use. Should we expose it at all? (@whitequark) + - vk2seb@: It can survive as a building block but I don't see why it needs to be exposed. Propose removal (reflected above). - `Decimal` and/or `Fraction` support? - This could make sense to have, but both can represent values that's not representable as binary fixed point. 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. + - vk2seb@: As both can represent values that aren't representable as binary fixed point, I don't see a huge benefit. I also can't immediately think of an application that would need >53 bit constants. I would propose for now we leave `Decimal` and `Fraction` out of scope of this RFC. - Name all the things. - Library name: @@ -154,6 +160,12 @@ TBD - `.int_bits`, `.frac_bits`? - cursed option: `int, frac = x.width`? - `.round()` is a bit awkwardly named when it's used both to increase and decrease precision. + - vk2seb@: The existing modifications address this: + - Library name: `lib.fixed` + - Type names and shapes: signature has now been updated to use `i_bits`, `f_bits` and the explicit underlying storage in the constructor for `fixed.Shape`. + - We now have both `.round()` and `.truncate()`. I don't think using the same name for increasing and decreasing precision is so bad. But if you feel strongly about this we may consider: + - Renaming them. + - Disallowing increasing precision with these methods, and add a new method for precision extension . ## Future possibilities [future-possibilities]: #future-possibilities From 191dc679d9db8a0358fc5c6a1b6d618b96f4fe83 Mon Sep 17 00:00:00 2001 From: Sebastian Holzapfel Date: Sat, 23 Nov 2024 18:06:40 +0100 Subject: [PATCH 11/13] fixed.Value: remove `.round()` completely. rename `.truncate()` to `.reshape()` --- text/0041-fixed-point.md | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/text/0041-fixed-point.md b/text/0041-fixed-point.md index 5213f84..077fc88 100644 --- a/text/0041-fixed-point.md +++ b/text/0041-fixed-point.md @@ -15,7 +15,7 @@ Add fixed point types to Amaranth. Fractional values in hardware are usually represented as some form of fixed point value. 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. -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. +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. ## Guide-level explanation [guide-level-explanation]: #guide-level-explanation @@ -58,11 +58,10 @@ The following operations are defined on it: - `.eq(value)`: Assign `value`. - If `value` is a `Value`, it'll be assigned directly to the underlying `Value`. - If `value` is an `int` or `float`, it'll be cast to a `fixed.Const` first. - - If `value` is a `fixed.Value`, the precision will be extended or rounded as required. -- `.round(f_bits=0)`: Return a new `fixed.Value` with precision changed to `f_bits`, rounding as required. - - Rounding strategy: round to nearest with ties rounded towards positive infinity. - - Under the hood, this involves truncating and adding the most significant truncated bit. -- `.truncate(f_bits=0)`: Return a new `fixed.Value` with precision changed to `f_bits`, truncating as required. + - If `value` is a `fixed.Value`, the precision will be extended or truncated as required. +- `.reshape(f_bits)`: Return a new `fixed.Value` with `f_bits` fractional bits, truncating or extending precision as required. +- `.reshape(shape)`: Return a new `fixed.Value` with shape `shape`, truncating or extending precision as required. + - For example, `value1.reshape(SQ(4, 4))` * value2 - `.__add__(other)`, `.__radd__(other)`, `.__sub__(other)`, `.__rsub__(other)`, `.__mul__(other)`, `.__rmul__(other)`: Binary arithmetic operators. - If `other` is a `Value`, it'll be cast to a `fixed.Value` first. - If `other` is an `int`, it'll be cast to a `fixed.Const` first. @@ -73,8 +72,8 @@ The following operations are defined on it: - `.__neg__()`, `.__pos__()`, `.__abs__()`: Unary arithmetic operators. - `.__lt__(other)`, `.__le__(other)`, `.__eq__(other)`, `.__ne__(other)`, `.__gt__(other)`, `.__ge__(other)`: Comparison operators. - Comparisons between `fixed.Value` of matching size, or between `fixed.Value` and `int` are permitted. - - Comparisons between `fixed.Value` of different widths are not permitted. - - Users are guided by an exception to explicitly `truncate()` or `round()` as needed. + - Comparisons between `fixed.Value` of different `f_bits` are not permitted. + - Users are guided by an exception to explicitly `reshape()` as needed. - Comparisons between `fixed.Value` and `float` are not permitted. - Users are guided by an exception to explicitly convert using `fixed.Const` as needed. @@ -86,7 +85,7 @@ The following additional operations are defined on it: - `fixed.Const(value, shape=None, clamp=False)`: Create a `fixed.Const` from `value`. `shape` must be a `fixed.Shape` if specified. - If `value` is an `int` and `shape` is not specified, the smallest shape that will fit `value` will be selected. - If `value` is a `float` and `shape` is not specified, the smallest shape that gives a perfect representation will be selected. - If `shape` is specified, `value` will be rounded to the closest representable value first. + If `shape` is specified, `value` will be truncated to the closest representable value first. - If `shape` is specified and `value` is too large to be represented by that shape, an exception is thrown. - The exception invites the user to try `clamp=True` to squash this exception, instead clamping the constant to the maximum / minimum value representable by the provided `shape`. - `.as_integer_ratio()`: Return the value represented as an integer ratio `tuple`. @@ -124,7 +123,7 @@ TBD - What should we do if a `float` is passed as `other` to an arithmetic operation? - We could use `float.as_integer_ratio()` to derive a perfect fixed point representation. - 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. + 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 truncated afterwards, the lower bits can affect rounding and thus won't be optimized out in synthesis. - We could use the same width for `other` as for `self`, adjusted to the appropriate exponent for the value. - We could outright reject it, requiring the user to explicitly specify precision like e.g. `value * Q(15).const(1 / 3)`. - vk2seb@: I would lean toward outright rejecting this, with an explicit cast necessary (now reflected above). @@ -135,7 +134,10 @@ TBD - IEEE 754 defaults to round to nearest, ties to even, which is more expensive to implement. - Should we make it user selectable? - We still need a default mode used when a higher precision number is passed to `.eq()`. - - vk2seb@: Both truncation and simple rounding (round to nearest) are commonly used in DSP algorithms. For now, we provide only `truncate()` and `round()` strategies (now reflected above). Additional rounding strategies may be added in a future RFC, however we will always need a default rounding strategy. + - samimia-swks@: In most DSP applications, simple truncating is done (bit picking, which is equivalent to a floor()) because it's free. I would vote for that being the default behavior at least. + - ld-cd@: (...) Truncate is still a reasonable default for most applications. + - ld-cd@: (...) I think a better approach would be to leave rounding and several other common operations that require platform dependent lowering to a subsequent RFC (...). + - vk2seb@: Both truncation and simple rounding (round to nearest) are commonly used in DSP algorithms. For now, we provide only `reshape()` (truncation, now reflected above). Additional rounding strategies may be added in a future RFC, however we will always need a default rounding strategy, and truncation seems like a sane default. - Are there any other operations that would be good to have? - From ld-cd@: `min()`, `max()` on `fixed.Shape` (vk2seb@: agree, heavily use this) @@ -163,9 +165,7 @@ TBD - vk2seb@: The existing modifications address this: - Library name: `lib.fixed` - Type names and shapes: signature has now been updated to use `i_bits`, `f_bits` and the explicit underlying storage in the constructor for `fixed.Shape`. - - We now have both `.round()` and `.truncate()`. I don't think using the same name for increasing and decreasing precision is so bad. But if you feel strongly about this we may consider: - - Renaming them. - - Disallowing increasing precision with these methods, and add a new method for precision extension . + - We now have `.reshape()`, which better represents increasing and decreasing precision. However, I'm open to new names. ## Future possibilities [future-possibilities]: #future-possibilities From 6d406f0eee0bfaca3109c9fd8fdc7e5d5847abed Mon Sep 17 00:00:00 2001 From: Sebastian Holzapfel Date: Sat, 23 Nov 2024 18:12:10 +0100 Subject: [PATCH 12/13] Unresolved: address __div__ --- text/0041-fixed-point.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/text/0041-fixed-point.md b/text/0041-fixed-point.md index 077fc88..f5d1689 100644 --- a/text/0041-fixed-point.md +++ b/text/0041-fixed-point.md @@ -164,9 +164,13 @@ TBD - `.round()` is a bit awkwardly named when it's used both to increase and decrease precision. - vk2seb@: The existing modifications address this: - Library name: `lib.fixed` - - Type names and shapes: signature has now been updated to use `i_bits`, `f_bits` and the explicit underlying storage in the constructor for `fixed.Shape`. + - Type names and shapes (from zyp@): signature has now been updated to use `i_bits`, `f_bits` and the explicit underlying storage in the constructor for `fixed.Shape`. - We now have `.reshape()`, which better represents increasing and decreasing precision. However, I'm open to new names. +- Should `__div__` be permitted? + - zyp@: (...) To avoid scope creep, I'm inclined to leave inferred division out of this RFC. We could instead do a separate RFC later for that. + - vk2seb@: agree, let's leave it out of this RFC. + ## Future possibilities [future-possibilities]: #future-possibilities From cb3d938753a6c22640bdb0befefd236300be9937 Mon Sep 17 00:00:00 2001 From: Sebastian Holzapfel Date: Sat, 23 Nov 2024 18:42:36 +0100 Subject: [PATCH 13/13] Rationale: add note on Q notation selected --- text/0041-fixed-point.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/text/0041-fixed-point.md b/text/0041-fixed-point.md index f5d1689..14afe85 100644 --- a/text/0041-fixed-point.md +++ b/text/0041-fixed-point.md @@ -110,8 +110,8 @@ TBD - 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. - Not having the sign bit included seems more common, and has the advantage that a number has the same fractional precision whether `i_bits` is 0 or not. - - Having the sign bit included is the dominant notation in the audio ASIC world (citation needed, comment from samimia-swks@). As of now, this RFC uses this notation as it is also a little simpler to reason about the size of underlying storage on constructing an `SQ`. - While Q notation names the signed type `Q`, it's more consistent for Amaranth to use `SQ` since other Amaranth types defaults to unsigned. + - vk2seb@: Having the sign bit included is the dominant notation in the audio ASIC world (citation needed, comment from samimia-swks@). As of now, this RFC uses this notation as it is also a little simpler to reason about the size of underlying storage on constructing an `SQ`. ## Prior art [prior-art]: #prior-art